From 55b0719bd987d2649fe7fd6793cab81d38dbbf07 Mon Sep 17 00:00:00 2001 From: Hannu Kamarainen Date: Wed, 15 Jan 2020 13:37:12 +0200 Subject: [PATCH 01/21] CSCFAIRMETA-122: [ADD] PAS catalog: Add groups for creating and editing --- src/metax_api/initialdata/datacatalogs.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/metax_api/initialdata/datacatalogs.json b/src/metax_api/initialdata/datacatalogs.json index 6913fd2d..d5d7cb8c 100644 --- a/src/metax_api/initialdata/datacatalogs.json +++ b/src/metax_api/initialdata/datacatalogs.json @@ -147,6 +147,8 @@ } }, { + "catalog_record_group_edit": "pas_edit", + "catalog_record_group_create": "pas_create", "catalog_json": { "title": { "en": "Fairdata PAS datasets", From 6e046be055013cc943410a0b8184756bf5310767 Mon Sep 17 00:00:00 2001 From: Katri Tegel Date: Mon, 5 Oct 2020 15:48:05 +0300 Subject: [PATCH 02/21] Merge test to stable (#639) (#640) * CSCFAIRMETA-647: [FIX] bug with almost simultaneous requests - LightFileserializer made modifications to class variable so when two requests came almost at the same time the latter was using modified version of the variable. Fixed with deepcopy - Changed app to use local Elasticsearch with tests since it is now working * CSCFAIRMETA-710: [FIX] _mapping endpoint and ES in testcases - Correctly call elasticsearch when request comes to _mapping endpoint - Tests can be run against local elasticsearch which reduces the population time - Follow naming convention in refdata loader with non-internal functions * CSCFAIRMETA-710: [FIX] remove unnecessary variable * CSCFAIRADM-337: [ADD] Update domain references (#631) - These mostly concern test data, but should be updated still * CSCFAIRMETA-704 [FIX] merge-draft-does-not-save-changes - In merge_draft take preservation_state from draft cr * CSCFAIRMETA-704-merge-draft-does-not-save-changes - Change metax-demo to metax.demo * CSCFAIRMETA-716: [FIX] create-draft-internal-error-when-draft-already-exists - Add a check for next_draft * CSCFAIRMETA-712-Issues-with-emails -Add migration to change email and organization name * CSCFAIRMETA-712-Issues-with-emails - Add check whether metadata_owner_org or metadata_provider_org exists * CSCFAIRMETA-636-return-if-files-are-part-of-some-dataset - Add ?keys=datasets|files and ?keysonly=bool - Remove parameter ?detailed as it is the same as ?keys=files * CSCFAIRMETA-636-return-if-files-are-part-of-some-dataset - Add test for parameter ?keysonly * CSCFAIRMETA-636-return-if-files-are-part-of-some-dataset - Add support for leaving argument ?detailed * CSCFAIRMETA-636-return-if-files-are-part-of-some-dataset Add ?keysonly return input list except standalone files or datasets without files * CSCFAIRMETA-636-return-if-files-are-part-of-some-dataset - Refactor sql's - Fix parameters 'request' and 'preferred_identifier' Co-authored-by: Tommi Pulli Co-authored-by: Tommi Pulli <43133956+tompulli@users.noreply.github.com> Co-authored-by: Mattias Levlin <11610718+MattiasLevlin@users.noreply.github.com> Co-authored-by: Tommi Pulli Co-authored-by: Tommi Pulli <43133956+tompulli@users.noreply.github.com> Co-authored-by: Mattias Levlin <11610718+MattiasLevlin@users.noreply.github.com> --- .travis-deploy.sh | 4 +- .../rest/base/schemas/att_dataset_schema.json | 2 +- .../schemas/harvester_dataset_schema.json | 2 +- .../rest/base/schemas/ida_dataset_schema.json | 2 +- .../base/serializers/common_serializer.py | 6 +- .../api/rest/base/views/file_view.py | 26 +++- .../v2/api_schemas/ida_dataset_schema.json | 2 +- .../rest/v2/schemas/att_dataset_schema.json | 2 +- .../v2/schemas/harvester_dataset_schema.json | 2 +- .../rest/v2/schemas/ida_dataset_schema.json | 2 +- .../api/rpc/base/views/elasticsearch_rpc.py | 13 +- .../migrations/0026_auto_20200917_1615.py | 88 +++++++++++++ src/metax_api/models/catalog_record_v2.py | 6 + src/metax_api/services/file_service.py | 122 ++++++++++-------- src/metax_api/settings.py | 6 +- .../tests/api/rest/base/views/files/read.py | 81 +++++++++++- .../api/rest/v2/views/datasets/drafts.py | 19 +++ .../tests/api/rest/v2/views/files/read.py | 79 +++++++++++- .../tests/testdata/generate_test_data.py | 2 +- src/metax_api/tests/testdata/test_data.json | 4 +- src/metax_api/utils/reference_data_loader.py | 8 +- 21 files changed, 387 insertions(+), 91 deletions(-) create mode 100644 src/metax_api/migrations/0026_auto_20200917_1615.py diff --git a/.travis-deploy.sh b/.travis-deploy.sh index 2f6b6784..0b94dc82 100755 --- a/.travis-deploy.sh +++ b/.travis-deploy.sh @@ -13,11 +13,11 @@ fi # if [[ "$TRAVIS_BRANCH" == "test" && "$TRAVIS_PULL_REQUEST" == "false" ]]; then # echo "Deploying to test.." # ansible-galaxy -r requirements.yml install --roles-path=roles -# ansible-playbook -vv -i inventories/test/hosts site_deploy.yml --extra-vars "ssh_user=metax-deploy-user server_domain_name=metax-test.csc.fi" +# ansible-playbook -vv -i inventories/test/hosts site_deploy.yml --extra-vars "ssh_user=metax-deploy-user server_domain_name=metax.fd-test.csc.fi" # elif [[ "$TRAVIS_BRANCH" == "stable" && "$TRAVIS_PULL_REQUEST" == "false" ]]; then # echo "Deploying to stable.." # ansible-galaxy -r requirements.yml install --roles-path=roles -# ansible-playbook -vv -i inventories/stable/hosts site_deploy.yml --extra-vars "ssh_user=metax-deploy-user server_domain_name=metax-stable.csc.fi" +# ansible-playbook -vv -i inventories/stable/hosts site_deploy.yml --extra-vars "ssh_user=metax-deploy-user server_domain_name=metax.fd-stable.csc.fi" # fi # # Make sure the last command to run before this part is the ansible-playbook command diff --git a/src/metax_api/api/rest/base/schemas/att_dataset_schema.json b/src/metax_api/api/rest/base/schemas/att_dataset_schema.json index 032d3010..5047bcbd 100644 --- a/src/metax_api/api/rest/base/schemas/att_dataset_schema.json +++ b/src/metax_api/api/rest/base/schemas/att_dataset_schema.json @@ -614,7 +614,7 @@ "place_uri":{ "@id":"http://purl.org/dc/terms/subject", "title":"Place uri", - "description":"URI for the place. Use reference data from: https://metax-test.csc.fi/es/reference_data/location/_search?pretty=true", + "description":"URI for the place. Use reference data from: https://metax.fd-test.csc.fi/es/reference_data/location/_search?pretty=true", "@type":"@id", "type":"object", "$ref":"#/definitions/Concept" diff --git a/src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json b/src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json index d3b2b9a3..55a099e6 100644 --- a/src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json +++ b/src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json @@ -718,7 +718,7 @@ "place_uri":{ "@id":"http://purl.org/dc/terms/subject", "title":"Place uri", - "description":"URI for the place. Use reference data from: https://metax-test.csc.fi/es/reference_data/location/_search?pretty=true", + "description":"URI for the place. Use reference data from: https://metax.fd-test.csc.fi/es/reference_data/location/_search?pretty=true", "@type":"@id", "type":"object", "$ref":"#/definitions/Concept" diff --git a/src/metax_api/api/rest/base/schemas/ida_dataset_schema.json b/src/metax_api/api/rest/base/schemas/ida_dataset_schema.json index e7f50bb2..810b92e1 100644 --- a/src/metax_api/api/rest/base/schemas/ida_dataset_schema.json +++ b/src/metax_api/api/rest/base/schemas/ida_dataset_schema.json @@ -762,7 +762,7 @@ "place_uri":{ "@id":"http://purl.org/dc/terms/subject", "title":"Place uri", - "description":"URI for the place. Use reference data from: https://metax-test.csc.fi/es/reference_data/location/_search?pretty=true", + "description":"URI for the place. Use reference data from: https://metax.fd-test.csc.fi/es/reference_data/location/_search?pretty=true", "@type":"@id", "type":"object", "$ref":"#/definitions/Concept" diff --git a/src/metax_api/api/rest/base/serializers/common_serializer.py b/src/metax_api/api/rest/base/serializers/common_serializer.py index a728cedd..a6d7d274 100644 --- a/src/metax_api/api/rest/base/serializers/common_serializer.py +++ b/src/metax_api/api/rest/base/serializers/common_serializer.py @@ -5,9 +5,10 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -import logging +from copy import deepcopy from collections import OrderedDict from datetime import datetime +import logging from django.db import transaction from django.db.models.query import QuerySet @@ -315,7 +316,8 @@ def ls_field_list(cls, received_field_list=[]): else: # get all fields - field_list = cls.allowed_fields + # deepcopy prevents inheriting classes from modifying the allowed_fields -set which should be static + field_list = deepcopy(cls.allowed_fields) return field_list diff --git a/src/metax_api/api/rest/base/views/file_view.py b/src/metax_api/api/rest/base/views/file_view.py index 1f798596..1fe3aaf9 100644 --- a/src/metax_api/api/rest/base/views/file_view.py +++ b/src/metax_api/api/rest/base/views/file_view.py @@ -166,18 +166,36 @@ def partial_update_bulk(self, request, *args, **kwargs): @action(detail=False, methods=['post'], url_path="datasets") def datasets(self, request): """ - Find out which datasets a list of files belongs to, and return their + keys=files: Find out which datasets a list of files belongs to, and return their metadata_version_identifiers as a list. + keys=datasets: Find out which files belong to a list of datasets, and return their + identifiers as a list. + + keysonly: return the same list as input, but only with files that belong to some dataset / + only with datasets that have files + The method is invoked using POST, because there are limits to length of query parameters in GET. Also, some clients forcibly shove parameters in body in GET requests to query parameters, so using POST instead is more guaranteed to work. """ - if CommonService.get_boolean_query_param(request, 'detailed'): - return FileService.get_detailed_datasets_where_file_belongs_to(request.data) + keysonly = CommonService.get_boolean_query_param(request, 'keysonly') + detailed = CommonService.get_boolean_query_param(request, 'detailed') + + params = request.query_params + + if not params.keys(): + return FileService.get_identifiers(request.data, 'noparams', True) + + if 'keys' in params.keys(): + if params['keys'] in ['files', 'datasets']: + return FileService.get_identifiers(request.data, params['keys'], keysonly) + + if detailed: # This can be removed as soon as front can listen to ?keys=files which returns the same + return FileService.get_identifiers(request.data, 'files', False) - return FileService.get_datasets_where_file_belongs_to(request.data) + raise Http403({ 'detail': [ 'Invalid parameters' ]}) @action(detail=False, methods=['post'], url_path="restore") def restore_files(self, request): diff --git a/src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json b/src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json index 482a1527..f2337862 100644 --- a/src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json +++ b/src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json @@ -761,7 +761,7 @@ "place_uri":{ "@id":"http://purl.org/dc/terms/subject", "title":"Place uri", - "description":"URI for the place. Use reference data from: https://metax-test.csc.fi/es/reference_data/location/_search?pretty=true", + "description":"URI for the place. Use reference data from: https://metax.fd-test.csc.fi/es/reference_data/location/_search?pretty=true", "@type":"@id", "type":"object", "$ref":"#/definitions/Concept" diff --git a/src/metax_api/api/rest/v2/schemas/att_dataset_schema.json b/src/metax_api/api/rest/v2/schemas/att_dataset_schema.json index 3b691f64..a0e67f41 100644 --- a/src/metax_api/api/rest/v2/schemas/att_dataset_schema.json +++ b/src/metax_api/api/rest/v2/schemas/att_dataset_schema.json @@ -561,7 +561,7 @@ "place_uri":{ "@id":"http://purl.org/dc/terms/subject", "title":"Place uri", - "description":"URI for the place. Use reference data from: https://metax-test.csc.fi/es/reference_data/location/_search?pretty=true", + "description":"URI for the place. Use reference data from: https://metax.fd-test.csc.fi/es/reference_data/location/_search?pretty=true", "@type":"@id", "type":"object", "$ref":"#/definitions/Concept" diff --git a/src/metax_api/api/rest/v2/schemas/harvester_dataset_schema.json b/src/metax_api/api/rest/v2/schemas/harvester_dataset_schema.json index 411a87cb..cc047bcb 100644 --- a/src/metax_api/api/rest/v2/schemas/harvester_dataset_schema.json +++ b/src/metax_api/api/rest/v2/schemas/harvester_dataset_schema.json @@ -665,7 +665,7 @@ "place_uri":{ "@id":"http://purl.org/dc/terms/subject", "title":"Place uri", - "description":"URI for the place. Use reference data from: https://metax-test.csc.fi/es/reference_data/location/_search?pretty=true", + "description":"URI for the place. Use reference data from: https://metax.fd-test.csc.fi/es/reference_data/location/_search?pretty=true", "@type":"@id", "type":"object", "$ref":"#/definitions/Concept" diff --git a/src/metax_api/api/rest/v2/schemas/ida_dataset_schema.json b/src/metax_api/api/rest/v2/schemas/ida_dataset_schema.json index 362106f6..3e59da3c 100644 --- a/src/metax_api/api/rest/v2/schemas/ida_dataset_schema.json +++ b/src/metax_api/api/rest/v2/schemas/ida_dataset_schema.json @@ -709,7 +709,7 @@ "place_uri":{ "@id":"http://purl.org/dc/terms/subject", "title":"Place uri", - "description":"URI for the place. Use reference data from: https://metax-test.csc.fi/es/reference_data/location/_search?pretty=true", + "description":"URI for the place. Use reference data from: https://metax.fd-test.csc.fi/es/reference_data/location/_search?pretty=true", "@type":"@id", "type":"object", "$ref":"#/definitions/Concept" diff --git a/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py b/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py index cf5974d3..bae36547 100644 --- a/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py +++ b/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py @@ -27,12 +27,21 @@ def map_refdata(self, request): if not isinstance(django_settings, dict): settings = django_settings.ELASTICSEARCH - connection_params = RDL._get_connection_parameters(settings) - esclient, scan = RDL._get_es_imports(settings['HOSTS'], connection_params) + connection_params = RDL.get_connection_parameters(settings) + # returns scan object as well but that is not needed here + esclient = RDL.get_es_imports(settings['HOSTS'], connection_params)[0] if not self.request.query_params: return Response(status=status.HTTP_200_OK) + elif '_mapping' in self.request.query_params: + try: + res = esclient.indices.get_mapping() + except Exception as e: + raise Http400(f'Error when accessing elasticsearch. {e}') + + return Response(data=res, status=status.HTTP_200_OK) + params = {} for k, v in self.request.query_params.items(): # python dict.items() keeps order so the url is always the first one diff --git a/src/metax_api/migrations/0026_auto_20200917_1615.py b/src/metax_api/migrations/0026_auto_20200917_1615.py new file mode 100644 index 00000000..0e185a77 --- /dev/null +++ b/src/metax_api/migrations/0026_auto_20200917_1615.py @@ -0,0 +1,88 @@ +# Generated by Django 3.0.7 on 2020-09-17 12:04 + +from django.db import migrations +import logging + +_logger = logging.getLogger(__name__) + +def change_emails(apps, schema_editor): + import json + CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') + + for cr in CatalogRecord.objects.all(): + owner = json.loads(json.dumps(cr.metadata_owner_org)) + provider = json.loads(json.dumps(cr.metadata_provider_org)) + + cr_changed = False + + owner_data = '' + prov_data = '' + if owner: + if 'tut.fi' in owner: + owner_data = owner.replace("tut.fi", "tuni.fi") + cr_changed = True + elif 'uta.fi' in owner: + owner_data = owner.replace("uta.fi", "tuni.fi") + cr_changed = True + if provider: + if 'tut.fi' in provider: + prov_data = provider.replace("tut.fi", "tuni.fi") + cr_changed = True + elif 'uta.fi' in provider: + prov_data = provider.replace("uta.fi", "tuni.fi") + cr_changed = True + + if cr_changed: + cr.metadata_owner_org = owner_data + cr.metadata_provider_org = prov_data + cr.save() + + # Change actor emails in research dataset + research_dataset_changed = False + + dataset = json.loads(json.dumps(cr.research_dataset)) + + roles = ['curator', 'creator', 'contributor', 'publisher', 'rights_holder'] + + for role in roles: + if role in dataset: + for actor in dataset[role]: + email = '' + if isinstance(actor, str): + if 'email' in dataset[role].keys(): + email = dataset[role]['email'] + if isinstance(actor, dict): + if 'email' in actor.keys(): + email = actor['email'] + + changed = False + if 'tut.fi' in email: + email = email.replace("tut.fi", "tuni.fi") + changed = True + elif 'uta.fi' in email: + email = email.replace("uta.fi", "tuni.fi") + changed = True + + if changed: + research_dataset_changed = True + if isinstance(actor, str): + dataset[role]['email'] = email + else: + actor['email'] = email + if research_dataset_changed: + cr.research_dataset = dataset + cr.save() + + +def revert(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0025_auto_20200811_1050'), + ] + + operations = [ + migrations.RunPython(change_emails, revert), + ] \ No newline at end of file diff --git a/src/metax_api/models/catalog_record_v2.py b/src/metax_api/models/catalog_record_v2.py index 6c74ba86..f0023eb9 100644 --- a/src/metax_api/models/catalog_record_v2.py +++ b/src/metax_api/models/catalog_record_v2.py @@ -303,6 +303,7 @@ def merge_draft(self): origin_cr = CatalogRecordV2.objects.get(next_draft_id=self.id) draft_cr = self + origin_cr.preservation_state = draft_cr.preservation_state origin_cr.date_modified = get_tz_aware_now_without_micros() origin_cr.user_modified = draft_cr.user_modified @@ -1135,6 +1136,11 @@ def create_draft(self): # a new dataset in draft state raise Http400('Dataset is already draft.') + if self.next_draft: + raise Http400( + 'The dataset already has an existing unmerged draft: {}'.format(self.next_draft.preferred_identifier) + ) + origin_cr = self draft_cr = origin_cr._create_new_dataset_version_template() diff --git a/src/metax_api/services/file_service.py b/src/metax_api/services/file_service.py index 0cecb493..2f435ad9 100644 --- a/src/metax_api/services/file_service.py +++ b/src/metax_api/services/file_service.py @@ -210,57 +210,37 @@ def restore_files(cls, request, file_identifier_list): return Response({ 'restored_files_count': affected_rows }, status=status.HTTP_200_OK) @classmethod - def get_datasets_where_file_belongs_to(cls, file_identifiers): + def get_identifiers(cls, identifiers, params, keysonly): """ - Find out which (non-deprecated) datasets a list of files belongs to, and return - their preferred_identifiers as a list. + keys='files': Find out which (non-deprecated) datasets a list of files belongs to, and return + their preferred_identifiers per file as a list in json format. + + keys='datasets': Find out which files belong to a list of datasets, and return + their preferred_identifiers per dataset as a list in json format. - Parameter file_identifiers can be a list of pk's (integers), or file identifiers (strings). + keysonly= for dataset return dataset ids that have files, for files return file ids that belong + to some dataset + + Parameter identifiers can be a list of pk's (integers), or file/dataset identifiers (strings). """ - _logger.info('Retrieving list of datasets where files belong to') + _logger.info('Retrieving detailed list of %s' % params) - file_ids = cls._file_identifiers_to_ids(file_identifiers) + ids = cls._identifiers_to_ids(identifiers, params) - _logger.info('Looking datasets for the following files (printing first 10):\n%s' - % '\n'.join(str(id) for id in file_identifiers[:10])) + _logger.info('Searching return for the following %s (printing first 10):\n%s' + % (params, '\n'.join(str(id) for id in ids[:10]))) - sql_select_related_records = """ + noparams = """ SELECT research_dataset->>'preferred_identifier' AS preferred_identifier FROM metax_api_catalogrecord cr INNER JOIN metax_api_catalogrecord_files cr_f ON catalogrecord_id = cr.id WHERE cr_f.file_id IN %s - AND cr.removed = false AND cr.active = true + AND cr.removed = false AND cr.active = true AND cr.deprecated = false GROUP BY preferred_identifier """ - with connection.cursor() as cr: - cr.execute(sql_select_related_records, [tuple(file_ids)]) - if cr.rowcount == 0: - preferred_identifiers = [] - _logger.info('No datasets found for files') - else: - preferred_identifiers = [ row[0] for row in cr.fetchall() ] - _logger.info('Found following datasets:\n%s' % '\n'.join(preferred_identifiers)) - - return Response(preferred_identifiers, status=status.HTTP_200_OK) - - @classmethod - def get_detailed_datasets_where_file_belongs_to(cls, file_identifiers): - """ - Find out which (non-deprecated) datasets a list of files belongs to, and return - their preferred_identifiers per file as a list in json format. - - Parameter file_identifiers can be a list of pk's (integers), or file identifiers (strings). - """ - _logger.info('Retrieving detailed list of datasets where files belong to') - - file_ids = cls._file_identifiers_to_ids(file_identifiers) - - _logger.info('Looking datasets for the following files (printing first 10):\n%s' - % '\n'.join(str(id) for id in file_identifiers[:10])) - - sql_select_related_records = """ + files = """ SELECT f.identifier, json_agg(cr.research_dataset->>'preferred_identifier') FROM metax_api_file f JOIN metax_api_catalogrecord_files cr_f @@ -268,20 +248,49 @@ def get_detailed_datasets_where_file_belongs_to(cls, file_identifiers): JOIN metax_api_catalogrecord cr ON cr.id=cr_f.catalogrecord_id WHERE f.id IN %s - AND cr.removed = false AND cr.active = true + AND cr.removed = false AND cr.active = true AND cr.deprecated = false GROUP BY f.id ORDER BY f.id ASC; """ + files_keysonly = files.replace(", json_agg(cr.research_dataset->>'preferred_identifier')", "") + + datasets = """ + SELECT cr.research_dataset->>'preferred_identifier', json_agg(f.identifier) + FROM metax_api_file f + JOIN metax_api_catalogrecord_files cr_f + ON f.id=cr_f.file_id + JOIN metax_api_catalogrecord cr + ON cr.id=cr_f.catalogrecord_id + WHERE cr.id IN %s + AND cr.removed = false AND cr.active = true AND cr.deprecated = false + GROUP BY cr.id + ORDER BY cr.id ASC; + """ + + datasets_keysonly = datasets.replace(", json_agg(f.identifier)", "") + + if keysonly: + sql = {'files': files_keysonly, 'datasets': datasets_keysonly, 'noparams': noparams} + else: + sql = {'files': files, 'datasets': datasets, 'noparams': noparams} + with connection.cursor() as cr: - cr.execute(sql_select_related_records, [tuple(file_ids)]) + cr.execute(sql[params], [tuple(ids)]) if cr.rowcount == 0: preferred_identifiers = [] - _logger.info('No datasets found for files') + _logger.info('No %s found for list of input identifiers' % params) else: preferred_identifiers = cr.fetchall() - _logger.info('Found following datasets:\n%s' % preferred_identifiers) - return Response(dict(preferred_identifiers), status=status.HTTP_200_OK) + _logger.info('Found following %s:\n%s' % (params, preferred_identifiers)) + + if keysonly: + list_of_keys = [] # This has to be here, cr.fetchall() returns a list of tuples which dict + for tuples in preferred_identifiers: # can't parse like below when second item is empty + list_of_keys.append(tuples[0]) + return Response(list_of_keys, status=status.HTTP_200_OK) + else: + return Response(dict(preferred_identifiers), status=status.HTTP_200_OK) @classmethod def destroy_single(cls, file): @@ -326,7 +335,7 @@ def destroy_bulk(cls, file_identifiers): """ _logger.info('Begin bulk delete files') - file_ids = cls._file_identifiers_to_ids(file_identifiers) + file_ids = cls._identifiers_to_ids(file_identifiers, 'noparams') deleted_files_count, project_identifier = cls._mark_files_as_deleted(file_ids) @@ -374,23 +383,30 @@ def delete_project(cls, project_id): return Response({ 'deleted_files_count': deleted_files_count }, status=status.HTTP_200_OK) @staticmethod - def _file_identifiers_to_ids(file_identifiers): + def _identifiers_to_ids(identifiers, params): """ - In case file_identifiers is identifiers (strings), which they probably are in real use, + In case identifiers is identifiers (strings), which they probably are in real use, do a query to get a list of pk's instead, since they will be used quite a few times. """ - if not isinstance(file_identifiers, list): + if not isinstance(identifiers, list): raise Http400('Received identifiers is not a list') - elif not file_identifiers: + elif not identifiers: _logger.info('Received empty list of identifiers. Aborting') raise Http400('Received empty list of identifiers') - elif isinstance(file_identifiers[0], int): - return file_identifiers + elif isinstance(identifiers[0], int): + return identifiers else: - ids = [ id for id in File.objects.filter(identifier__in=file_identifiers).values_list('id', flat=True) ] - if not ids: - raise Http404 - return ids + if params in ['files', 'noparams']: + ids = [ id for id in File.objects.filter(identifier__in=identifiers).values_list('id', flat=True) ] + if not ids: + raise Http404 + return ids + else: + cr = CatalogRecord + ids = [ id for id in cr.objects.filter(identifier__in=identifiers).values_list('id', flat=True) ] + if not ids: + raise Http404 + return ids @staticmethod def _mark_files_as_deleted(file_ids): diff --git a/src/metax_api/settings.py b/src/metax_api/settings.py index 033068c0..4638470a 100755 --- a/src/metax_api/settings.py +++ b/src/metax_api/settings.py @@ -521,9 +521,9 @@ } } -if executing_in_test_case or executing_in_travis: +if executing_in_travis: ELASTICSEARCH = { - 'HOSTS': ['metax-demo.fairdata.fi/es'], + 'HOSTS': ['metax.demo.fairdata.fi/es'], 'USE_SSL': True, 'ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART': True, } @@ -557,7 +557,7 @@ if executing_in_travis: OAI = { - 'BASE_URL': 'http://metax-test.csc.fi/oai/', + 'BASE_URL': 'http://metax.fd-test.csc.fi/oai/', 'BATCH_SIZE': 25, 'REPOSITORY_NAME': 'Metax', 'ETSIN_URL_TEMPLATE': 'http://etsin.something.fi/dataset/%s', diff --git a/src/metax_api/tests/api/rest/base/views/files/read.py b/src/metax_api/tests/api/rest/base/views/files/read.py index 1222bab6..3ee8d0b5 100644 --- a/src/metax_api/tests/api/rest/base/views/files/read.py +++ b/src/metax_api/tests/api/rest/base/views/files/read.py @@ -107,11 +107,35 @@ def test_get_related_datasets_ok_2(self): self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) + def test_keysonly(self): + """ + Parameter ?keysonly should return just values + """ + response = self.client.post('/rest/files/datasets?keys=files&keysonly', [1, 2, 121], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 2) # pid:urn:121 does not belong to any dataset + self.assertEqual(type(response.data), list, type(response.data)) # no dict keys + + response = self.client.post('/rest/files/datasets?keys=files&keysonly=false', [1, 2], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertEqual(type(response.data), dict, response.data) # Return by keys + + response = self.client.post('/rest/files/datasets?keys=datasets&keysonly', [1, 2, 14], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 2) # Only datasets 1 and 2 have files + self.assertEqual(type(response.data), list, type(response.data)) # no dict keys + def test_get_detailed_related_datasets_ok_1(self): """ - File identifiers listed below should belong to 5 datasets + File identifiers listed below should belong to 3 datasets """ - response = self.client.post('/rest/files/datasets?detailed=true', [1], format='json') + response = self.client.post('/rest/files/datasets?keys=files', [1], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 1) + self.assertEqual(len(list(response.data.values())[0]), 3, response.data) + + # Support for ?detailed + response = self.client.post('/rest/files/datasets?detailed', [1], format='json') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 1) self.assertEqual(len(list(response.data.values())[0]), 3, response.data) @@ -122,7 +146,7 @@ def test_get_detailed_related_datasets_ok_2(self): """ file_identifiers = [1, 2, 3, 4, 5] - response = self.client.post('/rest/files/datasets?detailed=true', file_identifiers, format='json') + response = self.client.post('/rest/files/datasets?keys=files', file_identifiers, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) @@ -132,13 +156,49 @@ def test_get_detailed_related_datasets_ok_2(self): # check if identifiers work file_identifiers = ['pid:urn:1', 'pid:urn:2', 'pid:urn:3', 'pid:urn:4', 'pid:urn:5'] - response = self.client.post('/rest/files/datasets?detailed=true', file_identifiers, format='json') + response = self.client.post('/rest/files/datasets?keys=files', file_identifiers, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) # set of all returned datasets self.assertEqual(len(set(sum(response.data.values(), []))), 5, response.data) + def test_get_detailed_related_files_ok_1(self): + """ + Dataset identifiers listed below should have 2 files + """ + response = self.client.post('/rest/files/datasets?keys=datasets', [1], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 1) + self.assertEqual(len(list(response.data.values())[0]), 2, response.data) + + def test_get_detailed_related_files_ok_2(self): + """ + Tests that datasets return files correctly + """ + dataset_identifiers = [1, 2, 3, 4, 5] + + response = self.client.post('/rest/files/datasets?keys=datasets', dataset_identifiers, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 5) + + # set of all returned datasets + self.assertEqual(len(set(sum(response.data.values(), []))), 10, response.data) + + # check if identifiers work + dataset_identifiers = ["cr955e904-e3dd-4d7e-99f1-3fed446f96d1", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d2", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d3", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d4", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d5"] + + response = self.client.post('/rest/files/datasets?keys=datasets', dataset_identifiers, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 5) + + # set of all returned datasets + self.assertEqual(len(set(sum(response.data.values(), []))), 10, response.data) + def test_get_related_datasets_files_not_found(self): """ When the files themselves are not found, 404 should be returned @@ -146,7 +206,11 @@ def test_get_related_datasets_files_not_found(self): response = self.client.post('/rest/files/datasets', ['doesnotexist'], format='json') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) - response = self.client.post('/rest/files/datasets?detailed=true', ['doesnotexist'], format='json') + response = self.client.post('/rest/files/datasets?keys=files', ['doesnotexist'], format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) + + # Support for ?detailed + response = self.client.post('/rest/files/datasets?detailed', ['doesnotexist'], format='json') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) def test_get_related_datasets_records_not_found(self): @@ -161,7 +225,12 @@ def test_get_related_datasets_records_not_found(self): self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 0) - response = self.client.post('/rest/files/datasets?detailed=true', [1], format='json') + response = self.client.post('/rest/files/datasets?keys=files', [1], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 0) + + # Support for ?detailed + response = self.client.post('/rest/files/datasets?detailed', [1], format='json') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 0) diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py b/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py index 5a9b69a7..eace5957 100644 --- a/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py @@ -399,6 +399,19 @@ def _merge_draft_changes(self, draft_id): draft_found = CR.objects_unfiltered.filter(pk=draft_id).exists() self.assertEqual(draft_found, False) + def test_create_draft_not_locked_before_merge(self): + """ + Tests that more than 1 temporary drafts can't be created + """ + cr = self._create_dataset() + # create draft + response = self.client.post('/rpc/v2/datasets/create_draft?identifier=%d' % cr['id'], format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + # create draft + response = self.client.post('/rpc/v2/datasets/create_draft?identifier=%d' % cr['id'], format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + def test_create_and_merge_draft(self): """ A simple test to create a draft, change some metadata, and publish the changes. @@ -409,6 +422,7 @@ def test_create_and_merge_draft(self): # create draft draft_cr = self._create_draft(cr['id']) draft_cr['research_dataset']['title']['en'] = 'modified title' + draft_cr['preservation_state'] = 20 # ensure original now has a link to next_draft response = self.client.get('/rest/v2/datasets/%s' % cr['id'], format="json") @@ -438,6 +452,11 @@ def test_create_and_merge_draft(self): draft_cr['research_dataset']['title'], response.data ) + self.assertEqual( + response.data['preservation_state'], + draft_cr['preservation_state'], + response.data + ) self.assertEqual('next_draft' in response.data, False, 'next_draft link should be gone') def test_missing_issued_date_is_generated_when_draft_is_merged(self): diff --git a/src/metax_api/tests/api/rest/v2/views/files/read.py b/src/metax_api/tests/api/rest/v2/views/files/read.py index 316fb64b..bc9453c5 100644 --- a/src/metax_api/tests/api/rest/v2/views/files/read.py +++ b/src/metax_api/tests/api/rest/v2/views/files/read.py @@ -107,11 +107,35 @@ def test_get_related_datasets_ok_2(self): self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) + def test_keysonly(self): + """ + Parameter ?keysonly should return just values + """ + response = self.client.post('/rest/files/datasets?keys=files&keysonly', [1, 2, 121], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 2) # pid:urn:121 does not belong to any dataset + self.assertEqual(type(response.data), list, type(response.data)) # no dict keys + + response = self.client.post('/rest/files/datasets?keys=files&keysonly=false', [1, 2], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertEqual(type(response.data), dict, response.data) # Return by keys + + response = self.client.post('/rest/files/datasets?keys=datasets&keysonly', [1, 2, 14], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 2) # Only datasets 1 and 2 have files + self.assertEqual(type(response.data), list, type(response.data)) # no dict keys + def test_get_detailed_related_datasets_ok_1(self): """ File identifiers listed below should belong to 5 datasets """ - response = self.client.post('/rest/v2/files/datasets?detailed=true', [1], format='json') + response = self.client.post('/rest/v2/files/datasets?keys=files', [1], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 1) + self.assertEqual(len(list(response.data.values())[0]), 3, response.data) + + # Support for ?detailed + response = self.client.post('/rest/v2/files/datasets?detailed', [1], format='json') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 1) self.assertEqual(len(list(response.data.values())[0]), 3, response.data) @@ -122,7 +146,7 @@ def test_get_detailed_related_datasets_ok_2(self): """ file_identifiers = [1, 2, 3, 4, 5] - response = self.client.post('/rest/v2/files/datasets?detailed=true', file_identifiers, format='json') + response = self.client.post('/rest/v2/files/datasets?keys=files', file_identifiers, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) @@ -132,13 +156,49 @@ def test_get_detailed_related_datasets_ok_2(self): # check if identifiers work file_identifiers = ['pid:urn:1', 'pid:urn:2', 'pid:urn:3', 'pid:urn:4', 'pid:urn:5'] - response = self.client.post('/rest/v2/files/datasets?detailed=true', file_identifiers, format='json') + response = self.client.post('/rest/v2/files/datasets?keys=files', file_identifiers, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) # set of all returned datasets self.assertEqual(len(set(sum(response.data.values(), []))), 5, response.data) + def test_get_detailed_related_files_ok_1(self): + """ + Dataset identifiers listed below should have 2 files + """ + response = self.client.post('/rest/v2/files/datasets?keys=datasets', [1], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 1) + self.assertEqual(len(list(response.data.values())[0]), 2, response.data) + + def test_get_detailed_related_files_ok_2(self): + """ + Tests that datasets return files correctly + """ + dataset_identifiers = [1, 2, 3, 4, 5] + + response = self.client.post('/rest/v2/files/datasets?keys=datasets', dataset_identifiers, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 5) + + # set of all returned datasets + self.assertEqual(len(set(sum(response.data.values(), []))), 10, response.data) + + # check if identifiers work + dataset_identifiers = ["cr955e904-e3dd-4d7e-99f1-3fed446f96d1", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d2", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d3", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d4", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d5"] + + response = self.client.post('/rest/v2/files/datasets?keys=datasets', dataset_identifiers, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 5) + + # set of all returned datasets + self.assertEqual(len(set(sum(response.data.values(), []))), 10, response.data) + def test_get_related_datasets_files_not_found(self): """ When the files themselves are not found, 404 should be returned @@ -146,7 +206,11 @@ def test_get_related_datasets_files_not_found(self): response = self.client.post('/rest/v2/files/datasets', ['doesnotexist'], format='json') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) - response = self.client.post('/rest/v2/files/datasets?detailed=true', ['doesnotexist'], format='json') + response = self.client.post('/rest/v2/files/datasets?keys=files', ['doesnotexist'], format='json') + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) + + # Support for ?detailed + response = self.client.post('/rest/v2/files/datasets?detailed', ['doesnotexist'], format='json') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) def test_get_related_datasets_records_not_found(self): @@ -161,7 +225,12 @@ def test_get_related_datasets_records_not_found(self): self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 0) - response = self.client.post('/rest/v2/files/datasets?detailed=true', [1], format='json') + response = self.client.post('/rest/v2/files/datasets?keys=files', [1], format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self._assert_results_length(response, 0) + + # Support for ?detailed + response = self.client.post('/rest/v2/files/datasets?detailed', [1], format='json') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 0) diff --git a/src/metax_api/tests/testdata/generate_test_data.py b/src/metax_api/tests/testdata/generate_test_data.py index d2ede21b..a364e61a 100644 --- a/src/metax_api/tests/testdata/generate_test_data.py +++ b/src/metax_api/tests/testdata/generate_test_data.py @@ -78,7 +78,7 @@ def generate_file_storages(file_storage_max_rows): 'file_storage_json': { 'title': title % str(i), 'identifier': identifier % str(i), - 'url': 'https://metax-test.csc.fi/rest/filestorages/%d' % i, + 'url': 'https://metax.fd-test.csc.fi/rest/filestorages/%d' % i, } }, 'model': "metax_api.filestorage", diff --git a/src/metax_api/tests/testdata/test_data.json b/src/metax_api/tests/testdata/test_data.json index 8a412d08..c9bac66e 100644 --- a/src/metax_api/tests/testdata/test_data.json +++ b/src/metax_api/tests/testdata/test_data.json @@ -6,7 +6,7 @@ "file_storage_json": { "identifier": "pid:urn:storageidentifier1", "title": "Title 1", - "url": "https://metax-test.csc.fi/rest/filestorages/1" + "url": "https://metax.fd-test.csc.fi/rest/filestorages/1" } }, "model": "metax_api.filestorage", @@ -19,7 +19,7 @@ "file_storage_json": { "identifier": "pid:urn:storageidentifier2", "title": "Title 2", - "url": "https://metax-test.csc.fi/rest/filestorages/2" + "url": "https://metax.fd-test.csc.fi/rest/filestorages/2" } }, "model": "metax_api.filestorage", diff --git a/src/metax_api/utils/reference_data_loader.py b/src/metax_api/utils/reference_data_loader.py index 55c5e0bd..df8347b9 100644 --- a/src/metax_api/utils/reference_data_loader.py +++ b/src/metax_api/utils/reference_data_loader.py @@ -72,8 +72,8 @@ def _fetch_reference_data(cls, settings): if not isinstance(settings, dict): settings = settings.ELASTICSEARCH - connection_params = cls._get_connection_parameters(settings) - esclient, scan = cls._get_es_imports(settings['HOSTS'], connection_params) + connection_params = cls.get_connection_parameters(settings) + esclient, scan = cls.get_es_imports(settings['HOSTS'], connection_params) reference_data = {} for index_name in esclient.indices.get_mapping().keys(): @@ -151,7 +151,7 @@ def _fetch_reference_data(cls, settings): return reference_data @staticmethod - def _get_connection_parameters(settings): + def get_connection_parameters(settings): """ https://docs.objectrocket.com/elastic_python_examples.html """ @@ -165,7 +165,7 @@ def _get_connection_parameters(settings): return {} @staticmethod - def _get_es_imports(hosts, conn_params): + def get_es_imports(hosts, conn_params): """ Returns correct version of the elasticsearch python client. This is needed in the transition between elasticsearch major versions because From ff4cb8777f139bd29d8c4bd321f169dba076fa90 Mon Sep 17 00:00:00 2001 From: Katri Tegel Date: Thu, 15 Oct 2020 09:58:12 +0300 Subject: [PATCH 03/21] CSCFAIRMETA-762-New-links-for-SYKE-datasets (#644) * CSCFAIRMETA-647: [FIX] bug with almost simultaneous requests - LightFileserializer made modifications to class variable so when two requests came almost at the same time the latter was using modified version of the variable. Fixed with deepcopy - Changed app to use local Elasticsearch with tests since it is now working * CSCFAIRMETA-710: [FIX] _mapping endpoint and ES in testcases - Correctly call elasticsearch when request comes to _mapping endpoint - Tests can be run against local elasticsearch which reduces the population time - Follow naming convention in refdata loader with non-internal functions * CSCFAIRMETA-710: [FIX] remove unnecessary variable * CSCFAIRADM-337: [ADD] Update domain references (#631) - These mostly concern test data, but should be updated still * CSCFAIRMETA-704 [FIX] merge-draft-does-not-save-changes - In merge_draft take preservation_state from draft cr * CSCFAIRMETA-704-merge-draft-does-not-save-changes - Change metax-demo to metax.demo * CSCFAIRMETA-716: [FIX] create-draft-internal-error-when-draft-already-exists - Add a check for next_draft * CSCFAIRMETA-712-Issues-with-emails -Add migration to change email and organization name * CSCFAIRMETA-712-Issues-with-emails - Add check whether metadata_owner_org or metadata_provider_org exists * CSCFAIRMETA-636-return-if-files-are-part-of-some-dataset - Add ?keys=datasets|files and ?keysonly=bool - Remove parameter ?detailed as it is the same as ?keys=files * CSCFAIRMETA-636-return-if-files-are-part-of-some-dataset - Add test for parameter ?keysonly * CSCFAIRMETA-636-return-if-files-are-part-of-some-dataset - Add support for leaving argument ?detailed * CSCFAIRMETA-636-return-if-files-are-part-of-some-dataset Add ?keysonly return input list except standalone files or datasets without files * CSCFAIRMETA-636-return-if-files-are-part-of-some-dataset - Refactor sql's - Fix parameters 'request' and 'preferred_identifier' * CSCFAIRMETA-762-New-links-for-SYKE-datasets - Save old notation into own field - Replace notation with new guid - Add datasets folder into migrations - Replace syke url for harvester Co-authored-by: Tommi Pulli Co-authored-by: Tommi Pulli <43133956+tompulli@users.noreply.github.com> Co-authored-by: Mattias Levlin <11610718+MattiasLevlin@users.noreply.github.com> --- .../api/oaipmh/base/metax_oai_server.py | 3 +- .../migrations/0027_auto_20201013_1404.py | 56 ++++ .../migrations/datasets/SYKE_csv.csv | 240 ++++++++++++++++++ 3 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 src/metax_api/migrations/0027_auto_20201013_1404.py create mode 100644 src/metax_api/migrations/datasets/SYKE_csv.csv diff --git a/src/metax_api/api/oaipmh/base/metax_oai_server.py b/src/metax_api/api/oaipmh/base/metax_oai_server.py index 89f3e6c0..a4f99ecf 100644 --- a/src/metax_api/api/oaipmh/base/metax_oai_server.py +++ b/src/metax_api/api/oaipmh/base/metax_oai_server.py @@ -17,7 +17,8 @@ from metax_api.services import CatalogRecordService as CRS from metax_api.services.datacite_service import DataciteException, convert_cr_to_datacite_cr_json -SYKE_URL_PREFIX_TEMPLATE = 'http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=%s' +# SYKE_IDENTIFIER_PREFIX +SYKE_URL_PREFIX_TEMPLATE = 'https://metadata.ymparisto.fi/dataset/%s' DATACATALOGS_SET = 'datacatalogs' DATASETS_SET = 'datasets' OAI_DC_MDPREFIX = 'oai_dc' diff --git a/src/metax_api/migrations/0027_auto_20201013_1404.py b/src/metax_api/migrations/0027_auto_20201013_1404.py new file mode 100644 index 00000000..5719b0b4 --- /dev/null +++ b/src/metax_api/migrations/0027_auto_20201013_1404.py @@ -0,0 +1,56 @@ +# Generated by Django 3.0.7 on 2020-10-13 11:04 + +from django.db import migrations +import csv +import json +import logging + +logger = logging.getLogger(__name__) + +def rename(apps, schema_editor): + CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') + + # Loop through csv + with open('metax_api/migrations/datasets/SYKE_csv.csv', newline='') as csvfile: + reader = csv.DictReader(csvfile, delimiter=";", quotechar='"') + + for row in reader: + old_guid = row["vanha guid"] + + if old_guid == 'vanha guid': # Check if is heade value + pass + else: + # Get dataset + try: + cr = CatalogRecord.objects.get(research_dataset__other_identifier__0__notation = old_guid) + new_guid = row["uusi guid"] + cr.research_dataset['other_identifier'][0]['notation'] = new_guid + cr.research_dataset['other_identifier'][0]['old_notation'] = old_guid + cr.user_modified = "migration_0027" + cr.save() + logger.info(f"successful migration of cr {cr.id} with new guid {new_guid} and old guid {old_guid}") + except CatalogRecord.DoesNotExist: + pass + +def revert(apps, schema_editor): + CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') + modified = CatalogRecord.objects.filter(user_modified="migration_0027") + for cr in modified: + try: + o_ids = cr.research_dataset['other_identifier'] + if "old_notation" in o_ids[0].keys(): + cr.research_dataset['other_identifier'][0]['notation'] = o_ids[0]["old_notation"] + del cr.research_dataset['other_identifier'][0]['old_notation'] + cr.save() + except CatalogRecord.DoesNotExist: + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0026_auto_20200917_1615'), + ] + + operations = [ + migrations.RunPython(rename, revert), + ] diff --git a/src/metax_api/migrations/datasets/SYKE_csv.csv b/src/metax_api/migrations/datasets/SYKE_csv.csv new file mode 100644 index 00000000..654a3edf --- /dev/null +++ b/src/metax_api/migrations/datasets/SYKE_csv.csv @@ -0,0 +1,240 @@ +title;vanha url;access;vanha guid;uusi guid;Omistaja;uusi url;; +Asuinalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E4DBC005-823B-469F-9AAB-51450438F41A};Avoin;{E4DBC005-823B-469F-9AAB-51450438F41A};{0B77FA8C-7C42-4783-AA5C-868152E32BE0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0B77FA8C-7C42-4783-AA5C-868152E32BE0};; +Chlorophyll-a daily product for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C6DCCB67-99B6-449A-A36F-26A847E6D856};Avoin;{C6DCCB67-99B6-449A-A36F-26A847E6D856};{10962669-987F-4FFA-9D73-A96E61AD6992};Finnish Environment Intitute (SYKE);https://metadata.ymparisto.fi/dataset/{10962669-987F-4FFA-9D73-A96E61AD6992};; +Chlorophyll-a daily product for the Baltic Sea 2012-2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={681B6DFF-2B4C-4881-AF69-37AD15A2B16A};Avoin;{681B6DFF-2B4C-4881-AF69-37AD15A2B16A};;;https://metadata.ymparisto.fi/dataset/;; +Chlorophyll-a weekly composite for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={422E4BF9-D15B-4779-8ABD-3610BD645C5C};Avoin;{422E4BF9-D15B-4779-8ABD-3610BD645C5C};;;https://metadata.ymparisto.fi/dataset/;; +Corine maanpeite 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A8936D40-2159-468D-84CD-0A978609B968};Avoin;{A8936D40-2159-468D-84CD-0A978609B968};{A70BC25F-43A2-4C33-A42D-9C22D9D4A3DB};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A70BC25F-43A2-4C33-A42D-9C22D9D4A3DB};; +Corine maanpeite 2006;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4438F7E1-2927-4854-B8F8-0EE8E8822C53};Avoin;{4438F7E1-2927-4854-B8F8-0EE8E8822C53};{703D5B2B-4CCC-49A7-B9E9-92B2E1290CD3};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{703D5B2B-4CCC-49A7-B9E9-92B2E1290CD3};; +Corine maanpeite 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D54C552F-F7F7-489B-8B1E-E093D93C7386};Avoin;{D54C552F-F7F7-489B-8B1E-E093D93C7386};{66D9A881-EE3C-42AD-9416-014EA6B84D23};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{66D9A881-EE3C-42AD-9416-014EA6B84D23};; +Harva ja tiheä taajama-alue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={674D6DB4-5FD9-434A-855F-F51316704630};Avoin;{674D6DB4-5FD9-434A-855F-F51316704630};{394B169F-2AE6-4966-8055-C593488F8898};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{394B169F-2AE6-4966-8055-C593488F8898};; +Havaitut tulva-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={62C30F1D-570D-4C75-80AE-6FE5402ECBBE};Avoin;{62C30F1D-570D-4C75-80AE-6FE5402ECBBE};{D8DE03E4-3F49-4BC1-9C58-3FADFB0F369A};;https://metadata.ymparisto.fi/dataset/{D8DE03E4-3F49-4BC1-9C58-3FADFB0F369A};; +Hydrologiset havaintopaikat, haihdunta-asemat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={72C2B39E-E581-4B5B-8C28-E5E0190DD97F};Avoin;{72C2B39E-E581-4B5B-8C28-E5E0190DD97F};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, jäänpaksuus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AB16A1FF-65E1-4A6E-A73D-653DDAAA7495};Avoin;{AB16A1FF-65E1-4A6E-A73D-653DDAAA7495};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, lumilinjamittaus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3D7A5268-E65C-4AB1-9A5E-99C8909AD764};Avoin;{3D7A5268-E65C-4AB1-9A5E-99C8909AD764};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, lämpötilaluotaus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={854C80BE-3383-4E67-8C21-0E4D038B7BA8};Avoin;{854C80BE-3383-4E67-8C21-0E4D038B7BA8};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, pintaveden lämpötila;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8462E50B-16BD-4C7F-9A79-F3DC6BFA1080};Avoin;{8462E50B-16BD-4C7F-9A79-F3DC6BFA1080};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, valunta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E11A44F-4987-4FF2-B55B-A3F94739FEE5};Avoin;{1E11A44F-4987-4FF2-B55B-A3F94739FEE5};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, vedenkorkeus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FC8A217-6A9D-4FE2-8923-843DA4A7DBE5};Avoin;{6FC8A217-6A9D-4FE2-8923-843DA4A7DBE5};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, virtaama;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B6AFC4CE-7827-46BE-9372-141CE59394AA};Avoin;{B6AFC4CE-7827-46BE-9372-141CE59394AA};;;https://metadata.ymparisto.fi/dataset/;; +INSPIRE-tietotuote: Luonnonsuojelu- ja erämaa-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DFC5480E-CFC4-4FF3-B42F-9685481309BC};Avoin;{DFC5480E-CFC4-4FF3-B42F-9685481309BC};;;https://metadata.ymparisto.fi/dataset/;; +INSPIRE-tietotuote: Natura 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EB7FEE73-535C-49D8-AD0B-EAF13E52E665};Avoin;{EB7FEE73-535C-49D8-AD0B-EAF13E52E665};;;https://metadata.ymparisto.fi/dataset/;; +INSPIRE-tietotuote: Uomaverkosto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6AF05F3B-0D56-49D5-BFEA-F1652620D879};Avoin;{6AF05F3B-0D56-49D5-BFEA-F1652620D879};;;https://metadata.ymparisto.fi/dataset/;; +Itämeren päivittäiset pintalämpötilat / Sea surface temperature for the Baltic Sea;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4568E834-419F-4295-8CB1-2BE76C1F3318};Avoin;{4568E834-419F-4295-8CB1-2BE76C1F3318};;;https://metadata.ymparisto.fi/dataset/;; +Itämeren viikottaiset pintalämpötilat / Sea surface temperature weekly composites for the Baltic Sea;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1B235B6E-170C-4EF4-BE20-77C606F1A165};Avoin;{1B235B6E-170C-4EF4-BE20-77C606F1A165};;;https://metadata.ymparisto.fi/dataset/;; +Järvien ja jokien luotauspisteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={28041FC2-F80F-43FF-BEAA-CE66D449D04F};Avoin;{28041FC2-F80F-43FF-BEAA-CE66D449D04F};{80B4FB0F-C60E-4290-8183-1309E8AD8B76};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{80B4FB0F-C60E-4290-8183-1309E8AD8B76};; +Järvien ja jokien syvyysaineisto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E3647EED-2697-4EDE-92C9-900DD458B87F};Avoin;{E3647EED-2697-4EDE-92C9-900DD458B87F};{DC4AD1E9-F4E5-4998-825D-D2CA4836A2F5};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{DC4AD1E9-F4E5-4998-825D-D2CA4836A2F5};; +Kaupunki-maaseutu-luokitus (YKR);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BDD349C3-3656-4B3C-A590-77901C058DA2};Avoin;{BDD349C3-3656-4B3C-A590-77901C058DA2};{A2556B8B-0E17-4E70-AF87-0FCEBDADBE89};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A2556B8B-0E17-4E70-AF87-0FCEBDADBE89};; +Keskustat ja kaupan alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7D2F780B-39D7-4C83-9A2A-C3E3D6534B61};Avoin;{7D2F780B-39D7-4C83-9A2A-C3E3D6534B61};{79B6DD92-3448-402D-95A9-D56B484CD6A1};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{79B6DD92-3448-402D-95A9-D56B484CD6A1};; +Koskiensuojelulailla suojellut vesistöt;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={48CD102E-D5CE-445A-B9DC-74F8E9345D94};Avoin;{48CD102E-D5CE-445A-B9DC-74F8E9345D94};{CF5DEA48-E8E4-49AA-8EDB-DBE862B92F07};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CF5DEA48-E8E4-49AA-8EDB-DBE862B92F07};; +Lajit, luontodirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4E1FF620-E911-4678-83A4-F4A13944BD1A};Avoin;{4E1FF620-E911-4678-83A4-F4A13944BD1A};{4F51E747-E3DC-4223-BD0D-50108396F8A1};;https://metadata.ymparisto.fi/dataset/{4F51E747-E3DC-4223-BD0D-50108396F8A1};; +Lake Chl-a products from Finland (MERIS, FRESHMON);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=CHL_Finland_SYKE_2006-2011_MERIS_FUB_RECT;Avoin;CHL_Finland_SYKE_2006-2011_MERIS_FUB_RECT;;;https://metadata.ymparisto.fi/dataset/;; +Liikennealueiden ympäristöriskiluokitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F41D131D-B103-4726-B291-E43B49D136A2};Avoin;{F41D131D-B103-4726-B291-E43B49D136A2};{0143CB1F-4203-4252-B042-E32CB570A930};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0143CB1F-4203-4252-B042-E32CB570A930};; +Linnut, lintudirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={084531FC-EE76-418A-9832-5F8B418BA60E};Avoin;{084531FC-EE76-418A-9832-5F8B418BA60E};{EEE47DAE-72EF-403A-AED4-7F5E78E53739};;https://metadata.ymparisto.fi/dataset/{EEE47DAE-72EF-403A-AED4-7F5E78E53739};; +Luonnonsuojelu- ja erämaa-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2627E9FE-B657-48E1-A98D-000D4CD5CA38};Avoin;{2627E9FE-B657-48E1-A98D-000D4CD5CA38};{C8FC4A42-A2C3-40C4-92CD-2299C688514E};Metsähallitus;https://metadata.ymparisto.fi/dataset/{C8FC4A42-A2C3-40C4-92CD-2299C688514E};; +Luonnonsuojeluohjelma-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C305FA65-F319-4FA0-AAB8-F92AE32B6EE2};Avoin;{C305FA65-F319-4FA0-AAB8-F92AE32B6EE2};{5A93FF5B-FB88-40C7-84E0-91FCC492A621};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5A93FF5B-FB88-40C7-84E0-91FCC492A621};; +Luontotyypit, luontodirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B67BC862-E17C-42A2-B7AE-70B8A171170F};Avoin;{B67BC862-E17C-42A2-B7AE-70B8A171170F};{A0DF9C40-CC7B-4359-8595-5C76B760257C};;https://metadata.ymparisto.fi/dataset/{A0DF9C40-CC7B-4359-8595-5C76B760257C};; +Maasto- ja vesiliikenteen rajoitusalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={01A88DB7-7672-4671-95C4-3CD2AEAC5A99};Avoin;{01A88DB7-7672-4671-95C4-3CD2AEAC5A99};{AFBC7507-2167-4224-8CBA-37E1B6C975A3};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AFBC7507-2167-4224-8CBA-37E1B6C975A3};; +Maisemamaakuntajako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8D4119C7-23C6-419C-B874-2FB4C8DAEC44};Avoin;{8D4119C7-23C6-419C-B874-2FB4C8DAEC44};{27ACED25-8784-484D-962B-3207459D8BF0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{27ACED25-8784-484D-962B-3207459D8BF0};; +Metsäkasvillisuusvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={56486ECE-92E9-4FA5-96F3-0FC1D1B8C428};Avoin;{56486ECE-92E9-4FA5-96F3-0FC1D1B8C428};{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};; +Metsäkasvillisuusvyöhykkeet uhanalaisarviointia varten;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FBF9037A-3381-47C4-AD09-BBEAB007FEC0};Avoin;{FBF9037A-3381-47C4-AD09-BBEAB007FEC0};{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};;https://metadata.ymparisto.fi/dataset/{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};; +Natura2000 alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={36FA93DC-F156-427E-973E-E83F7AFC6306};Avoin;{36FA93DC-F156-427E-973E-E83F7AFC6306};{ED80465E-135B-4391-AA8A-FE2038FB224D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{ED80465E-135B-4391-AA8A-FE2038FB224D};; +Pohjavesialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F8321822-0A0D-4AC4-81E3-B9E3BE6386A2};Avoin;{F8321822-0A0D-4AC4-81E3-B9E3BE6386A2};{8F45F7BF-669F-4434-A8DB-8E686933F6FF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8F45F7BF-669F-4434-A8DB-8E686933F6FF};; +Rakennussuojelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1500436E-FD0E-4560-A933-847F16EFCC94};Avoin;{1500436E-FD0E-4560-A933-847F16EFCC94};{46AC1915-EC9D-4A02-92C0-58DAD8301E4A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{46AC1915-EC9D-4A02-92C0-58DAD8301E4A};; +Ranta10 - rantaviiva 1:10 000 ja uomaverkosto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A40A94CB-4905-4489-9C03-52B6CE9F66CD};Avoin;{A40A94CB-4905-4489-9C03-52B6CE9F66CD};{E15CD0B3-3934-4ABC-BF23-A8C622FA6A57};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E15CD0B3-3934-4ABC-BF23-A8C622FA6A57};; +Residential Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={593D9E64-4289-4B7B-A946-7A59C763C904};Avoin;{593D9E64-4289-4B7B-A946-7A59C763C904};;;https://metadata.ymparisto.fi/dataset/;; +Satelliittikuvamosaiikki (Image2000);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AF0A925D-9410-4431-8DB8-0455C73D193E};Avoin;{AF0A925D-9410-4431-8DB8-0455C73D193E};{0FD2F424-2C22-4384-A0C6-65AF19DED2A9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0FD2F424-2C22-4384-A0C6-65AF19DED2A9};; +Satelliittikuvamosaiikki (Image2006);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F1B27979-9B92-4607-B97F-28A5808AD5CC};Avoin;{F1B27979-9B92-4607-B97F-28A5808AD5CC};{5F95B099-0EAC-44DC-A1D5-B5B4A388A862};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5F95B099-0EAC-44DC-A1D5-B5B4A388A862};; +Satelliittikuvamosaiikki (Image2012);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={85FE6E30-90D2-4DB9-901B-7D86A74599DD};Avoin;{85FE6E30-90D2-4DB9-901B-7D86A74599DD};{94D48E72-5150-4D21-86E6-361506DAD28A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{94D48E72-5150-4D21-86E6-361506DAD28A};; +Siltojen korkeudet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7E8EA280-EAD4-4AFF-BA43-73DCA7805836};Avoin;{7E8EA280-EAD4-4AFF-BA43-73DCA7805836};{07943EAC-6AF9-4EB1-9B31-B47199446476};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{07943EAC-6AF9-4EB1-9B31-B47199446476};; +Soidensuojelun täydennysehdotus, Etelä-Suomi ja valtionmaan toteutuneet kohteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={5F14DCDF-6A2C-4A67-9035-BC73D0ED60B0};Avoin;{5F14DCDF-6A2C-4A67-9035-BC73D0ED60B0};{C0D331FF-68D8-4BD4-AEB3-B631AF824745};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C0D331FF-68D8-4BD4-AEB3-B631AF824745};; +Suokasvillisuusvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B6BD1CAD-BAB5-4473-AFD0-79DB30A88420};Avoin;{B6BD1CAD-BAB5-4473-AFD0-79DB30A88420};{193219CE-197F-4717-8D40-E7EDF6884A1E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{193219CE-197F-4717-8D40-E7EDF6884A1E};; +Tulvariskialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={83B57B72-1D1D-4DE4-BC61-180F3EE028AF};Avoin;{83B57B72-1D1D-4DE4-BC61-180F3EE028AF};{AF913A8D-484D-4313-ABD4-AE65A46A391E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AF913A8D-484D-4313-ABD4-AE65A46A391E};; +Tulvavaaravyöhykkeet, meritulva;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9EC02206-E14C-4186-AF09-829FFFB43FD9};Avoin;{9EC02206-E14C-4186-AF09-829FFFB43FD9};;;https://metadata.ymparisto.fi/dataset/;; +Tulvavaaravyöhykkeet, vesistötulva (avovesi);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C2B3FC73-060B-4053-BA04-54D86F4DE7E5};Avoin;{C2B3FC73-060B-4053-BA04-54D86F4DE7E5};;;https://metadata.ymparisto.fi/dataset/;; +Tunturialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A9B5E4AF-21B3-4FA5-AF45-2CD819CE742E};Avoin;{A9B5E4AF-21B3-4FA5-AF45-2CD819CE742E};{ADEAFCBE-9E2D-403A-8E78-13FE913D5E42};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{ADEAFCBE-9E2D-403A-8E78-13FE913D5E42};; +Uimavesidirektiivin mukaiset uimavedet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={918099FC-918C-4FA3-9444-5A3D5E950877};Avoin;{918099FC-918C-4FA3-9444-5A3D5E950877};{11F3E166-1B35-4628-BE18-45D1A09A097F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{11F3E166-1B35-4628-BE18-45D1A09A097F};; +Valtakunnallisesti arvokkaat kallioalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4CFB1D5B-F47E-4E92-918A-63EC5BB9C217};Avoin;{4CFB1D5B-F47E-4E92-918A-63EC5BB9C217};{626BAE8B-6632-43FD-8B3A-CF1E01662331};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{626BAE8B-6632-43FD-8B3A-CF1E01662331};; +Valtakunnallisesti arvokkaat moreenimuodostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A21026BF-204C-42FC-9FCB-450BD5A94C81};Avoin;{A21026BF-204C-42FC-9FCB-450BD5A94C81};{8253487E-FA94-4E83-9A85-71F85D4DA0B0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8253487E-FA94-4E83-9A85-71F85D4DA0B0};; +Valtakunnallisesti arvokkaat tuuli- ja rantakerrostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={66A5653E-A8E7-4BA3-9703-A0AF001AE089};Avoin;{66A5653E-A8E7-4BA3-9703-A0AF001AE089};{3298587D-C391-47C6-A4E0-D599D0206B73};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3298587D-C391-47C6-A4E0-D599D0206B73};; +Valuma-aluejako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={343C40A6-D0ED-4B6A-BA77-2724A48A3BD3};Avoin;{343C40A6-D0ED-4B6A-BA77-2724A48A3BD3};{44394B13-85D7-4998-BD06-8ADC77C7455C};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{44394B13-85D7-4998-BD06-8ADC77C7455C};; +Water turbidity daily product for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={89F4653E-9930-4540-BFBA-C78444E1FCD5};Avoin;{89F4653E-9930-4540-BFBA-C78444E1FCD5};{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};Finnish Environment Intitute (SYKE);https://metadata.ymparisto.fi/dataset/{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};; +Water turbidity daily product for the Baltic Sea 2012-2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B4783610-3298-4550-9793-9DBDCB0080B1};Avoin;{B4783610-3298-4550-9793-9DBDCB0080B1};;;https://metadata.ymparisto.fi/dataset/;; +Water turbidity weekly composite for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CEF2EA78-0400-46F1-8173-CD1D1EE547D1};Avoin;{CEF2EA78-0400-46F1-8173-CD1D1EE547D1};{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};;https://metadata.ymparisto.fi/dataset/{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};; +Vesienhoitoalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DFA8CF50-721B-4C71-93DD-A8264C5C2A9C};Avoin;{DFA8CF50-721B-4C71-93DD-A8264C5C2A9C};{6AF42DE4-EB9A-4568-9E6C-90BEF19673D0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6AF42DE4-EB9A-4568-9E6C-90BEF19673D0};; +Vesipuitedirektiivin mukaiset vesimuodostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CB8FEF32-5FCE-4E33-A8B8-1FBB02F9C2C1};Avoin;{CB8FEF32-5FCE-4E33-A8B8-1FBB02F9C2C1};{A3748B80-6E63-4460-A8B7-72CB9884F69B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A3748B80-6E63-4460-A8B7-72CB9884F69B};; +Yhdyskuntarakenteen vyöhykkeet 2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={96F338EA-75AF-432C-A780-31A3CDECBDF2};Avoin;{96F338EA-75AF-432C-A780-31A3CDECBDF2};{3B8BD61A-C137-46ED-B16A-B8BA27D1561B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3B8BD61A-C137-46ED-B16A-B8BA27D1561B};; +YKR Kaupunkiseudut;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={29205135-177D-4244-ABF3-BEE74782E567};Avoin;{29205135-177D-4244-ABF3-BEE74782E567};{E3E29357-4333-4C11-8E0E-FCD7307F91EE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E3E29357-4333-4C11-8E0E-FCD7307F91EE};; +YKR-aluejaot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={802F4259-8AB3-473A-810C-FA3BE4FE5C92};Avoin;{802F4259-8AB3-473A-810C-FA3BE4FE5C92};{A2CEE8A2-F188-4787-88FC-A393032CA60A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A2CEE8A2-F188-4787-88FC-A393032CA60A};; +YKR-taajama;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CB02E608-6E6B-417E-87DA-BD7C8CB53712};Avoin;{CB02E608-6E6B-417E-87DA-BD7C8CB53712};{28F79998-B5E2-4085-AF80-FB3FA5E9F9BA};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{28F79998-B5E2-4085-AF80-FB3FA5E9F9BA};; +Ympäristömeludirektiivin mukaiset melualueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9D872FA-3A6D-45C6-AD52-78E0724307A7};Avoin;{E9D872FA-3A6D-45C6-AD52-78E0724307A7};;;https://metadata.ymparisto.fi/dataset/;; +Ympäristömeludirektiivin mukaiset melualueet 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8B962E44-AB11-4D38-B178-76AB85B114BB};Avoin;{8B962E44-AB11-4D38-B178-76AB85B114BB};{FCF0230E-07C1-48FF-A762-B51F1332F1DC};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{FCF0230E-07C1-48FF-A762-B51F1332F1DC};; +Aluesadannan ja lumen aluevesiarvon laskenta-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9CC3C6F8-D834-4663-8424-D846CEE43F3D};Saatavuutta rajoitettu;{9CC3C6F8-D834-4663-8424-D846CEE43F3D};{248E7E3F-02A6-41AF-A977-5A4949966001};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{248E7E3F-02A6-41AF-A977-5A4949966001};; +ARA-rakennukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E97AFFB-BFDB-4077-BEDD-BE5A7D971C8E};Saatavuutta rajoitettu;{1E97AFFB-BFDB-4077-BEDD-BE5A7D971C8E};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Areas Included in National Conservation Programmes;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A0C487D4-968B-4553-9EFB-870D6D2A728C};Saatavuutta rajoitettu;{A0C487D4-968B-4553-9EFB-870D6D2A728C};;;https://metadata.ymparisto.fi/dataset/;; +Areas with Local Detailed Plans;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E60A3152-F0AA-4E61-BA1C-E28F0DBE8FF6};Saatavuutta rajoitettu;{E60A3152-F0AA-4E61-BA1C-E28F0DBE8FF6};;;https://metadata.ymparisto.fi/dataset/;; +Asemakaavoitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AC7E17C3-0833-40B1-B37A-187320A03826};Saatavuutta rajoitettu;{AC7E17C3-0833-40B1-B37A-187320A03826};{31968F40-0083-413A-B474-A6D2DB081EDF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{31968F40-0083-413A-B474-A6D2DB081EDF};; +Asemakaavojen seurantalomakkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9BCE41E0-54BC-43A4-9543-D3948FD151EF};Saatavuutta rajoitettu;{9BCE41E0-54BC-43A4-9543-D3948FD151EF};{9BCE41E0-54BC-43A4-9543-D3948FD151EF};Suomen ympäristökeskus / Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{9BCE41E0-54BC-43A4-9543-D3948FD151EF};; +Asianhallintajärjestelmä AHJO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DBBDEECB-AB2A-4B01-B45B-DE809992F79F};Saatavuutta rajoitettu;{DBBDEECB-AB2A-4B01-B45B-DE809992F79F};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Asuinrakennusten tiheys (rakennusta/km2);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={44F16A49-3DC8-4BB5-9420-6D015FB64EF1};Saatavuutta rajoitettu;{44F16A49-3DC8-4BB5-9420-6D015FB64EF1};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +AVOIMUUSINDEKSI MERENPOHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0189B2FD-4411-4CB0-90E5-DF4E1293C6F3};Saatavuutta rajoitettu;{0189B2FD-4411-4CB0-90E5-DF4E1293C6F3};{EB99F934-DCF8-4383-BD38-73B2DF3F0C72};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{EB99F934-DCF8-4383-BD38-73B2DF3F0C72};; +Bathing Waters;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1D154349-AFCA-4FBB-AD07-50EB6F36339D};Saatavuutta rajoitettu;{1D154349-AFCA-4FBB-AD07-50EB6F36339D};;;https://metadata.ymparisto.fi/dataset/;; +BCU-konttipaikat (öljyyntyneiden lintujen hoitoon);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EFC9B874-90BF-4701-BABF-29F6F7D58BE0};Saatavuutta rajoitettu;{EFC9B874-90BF-4701-BABF-29F6F7D58BE0};{DF0A5826-F1DF-42EB-A7A8-B8510A46B675};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{DF0A5826-F1DF-42EB-A7A8-B8510A46B675};; +BORIS 2 (Baltic Oil Response Information System);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={180ACCFE-FE84-4CD3-AF8A-6FF75C473059};Saatavuutta rajoitettu;{180ACCFE-FE84-4CD3-AF8A-6FF75C473059};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Catchment areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D6C6858A-562D-4965-AD77-2B1E97EFFA6B};Saatavuutta rajoitettu;{D6C6858A-562D-4965-AD77-2B1E97EFFA6B};;;https://metadata.ymparisto.fi/dataset/;; +CERAMIUM TENUICORNE LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D30F72F9-01F0-448A-9F18-213954BF87C7};Saatavuutta rajoitettu;{D30F72F9-01F0-448A-9F18-213954BF87C7};{F274E613-65AB-4867-B2C6-925596B7A8DE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F274E613-65AB-4867-B2C6-925596B7A8DE};; +CHARA ASPERA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F271AB26-3268-461F-A380-F3A1CC6AB62E};Saatavuutta rajoitettu;{F271AB26-3268-461F-A380-F3A1CC6AB62E};{BF849316-273C-4347-B151-D4F06460C314};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF849316-273C-4347-B151-D4F06460C314};; +CHARA SPP. & NITELLA SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={31CF9F00-FAA6-4158-B2E9-9D443776C4B1};Saatavuutta rajoitettu;{31CF9F00-FAA6-4158-B2E9-9D443776C4B1};;;https://metadata.ymparisto.fi/dataset/;; +CHARA TOMENTOSA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3895111B-8E40-4EC1-9256-B4C59B460832};Saatavuutta rajoitettu;{3895111B-8E40-4EC1-9256-B4C59B460832};{0408513A-8CCF-422F-A47B-F12F0946D6EA};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0408513A-8CCF-422F-A47B-F12F0946D6EA};; +CHORDA FILUM LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7D430B8-AEA0-4701-BD8F-533892EA0AD4};Saatavuutta rajoitettu;{B7D430B8-AEA0-4701-BD8F-533892EA0AD4};{CE550B14-5568-4C84-A67C-1473FC33E71F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CE550B14-5568-4C84-A67C-1473FC33E71F};; +CLADOPHORA GLOMERATA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={609B6F9F-954E-4F98-BCFC-172689200290};Saatavuutta rajoitettu;{609B6F9F-954E-4F98-BCFC-172689200290};{411FD599-AAC9-491D-95CA-BC942AE2FF72};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{411FD599-AAC9-491D-95CA-BC942AE2FF72};; +CLADOPHORA RUPESTRIS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0B5AAC11-E3E2-40D3-800F-8F5801F503AB};Saatavuutta rajoitettu;{0B5AAC11-E3E2-40D3-800F-8F5801F503AB};{37D789D3-A7E2-44FA-B227-58B59F962244};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{37D789D3-A7E2-44FA-B227-58B59F962244};; +Corine Land Cover 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EEC35D81-765A-4A7B-BC9B-F89A476807EC};Saatavuutta rajoitettu;{EEC35D81-765A-4A7B-BC9B-F89A476807EC};;;https://metadata.ymparisto.fi/dataset/;; +Corine Land Cover 2006;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DD648E4F-0478-4D32-8286-C2437EC87D33};Saatavuutta rajoitettu;{DD648E4F-0478-4D32-8286-C2437EC87D33};;;https://metadata.ymparisto.fi/dataset/;; +Corine Land Cover 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A58329D8-3ED0-47B8-B789-3095EE55C255};Saatavuutta rajoitettu;{A58329D8-3ED0-47B8-B789-3095EE55C255};;;https://metadata.ymparisto.fi/dataset/;; +Elinympäristön tietopalvelu Liiteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={629AC114-8BBC-48B5-9D8F-303A53CF3E9F};Saatavuutta rajoitettu;{629AC114-8BBC-48B5-9D8F-303A53CF3E9F};{1893C260-D934-48F8-A0A0-3606FC392438};Suomen ympäristökeskus / Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{1893C260-D934-48F8-A0A0-3606FC392438};; +Eliölajit-tietojärjestelmä TAXON;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C420898F-600A-41CB-8843-F06A90301427};Saatavuutta rajoitettu;{C420898F-600A-41CB-8843-F06A90301427};{C420898F-600A-41CB-8843-F06A90301427};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C420898F-600A-41CB-8843-F06A90301427};; +Energiantuotanto rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9707160B-8B54-48DB-9151-4A41FD46D159};Saatavuutta rajoitettu;{9707160B-8B54-48DB-9151-4A41FD46D159};{DBEAADBD-3EE0-498D-8EC2-C66191DA559C};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{DBEAADBD-3EE0-498D-8EC2-C66191DA559C};; +Environmental Noise Maps;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0CF94C00-D59A-45A0-A2EA-4ACF9014A2BA};Saatavuutta rajoitettu;{0CF94C00-D59A-45A0-A2EA-4ACF9014A2BA};;;https://metadata.ymparisto.fi/dataset/;; +EPHYDATIA FLUVIATILIS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB5AE7E2-8C84-4FCC-83DD-1150F60EEE27};Saatavuutta rajoitettu;{BB5AE7E2-8C84-4FCC-83DD-1150F60EEE27};{20637AA7-97D7-43AB-AF34-636043F1C1B9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{20637AA7-97D7-43AB-AF34-636043F1C1B9};; +Etäisyysvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={64764F78-0F35-438B-9EA4-FDAC3384840D};Saatavuutta rajoitettu;{64764F78-0F35-438B-9EA4-FDAC3384840D};{BF51D501-58F4-4932-A951-5B5A88F4D3C6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF51D501-58F4-4932-A951-5B5A88F4D3C6};; +Flood hazard areas, lake and river floods;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={302A9608-99B9-4D10-A469-36949D12436F};Saatavuutta rajoitettu;{302A9608-99B9-4D10-A469-36949D12436F};;;https://metadata.ymparisto.fi/dataset/;; +Flood hazard areas, sea floods;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6C21A825-FE70-47CE-91CA-E14EBE01F562};Saatavuutta rajoitettu;{6C21A825-FE70-47CE-91CA-E14EBE01F562};;;https://metadata.ymparisto.fi/dataset/;; +Flood Risk Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={13972098-7333-4827-B5D0-CFEF3CE54295};Saatavuutta rajoitettu;{13972098-7333-4827-B5D0-CFEF3CE54295};;;https://metadata.ymparisto.fi/dataset/;; +FONTINALIS SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={83CE79F8-4DF5-4B8A-B431-77FAC6195603};Saatavuutta rajoitettu;{83CE79F8-4DF5-4B8A-B431-77FAC6195603};{6EF6BCA9-D30B-45C1-8111-641DC55FA095};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6EF6BCA9-D30B-45C1-8111-641DC55FA095};; +Forest Vegetation Zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D53E9674-A446-4F54-81C3-8C15310353CC};Saatavuutta rajoitettu;{D53E9674-A446-4F54-81C3-8C15310353CC};;;https://metadata.ymparisto.fi/dataset/;; +FUCUS SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FB02515B-D7A9-4F6D-BD15-2E2B818AD997};Saatavuutta rajoitettu;{FB02515B-D7A9-4F6D-BD15-2E2B818AD997};{B6B48CE3-B9FA-442A-9FF5-F8E0B396578D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{B6B48CE3-B9FA-442A-9FF5-F8E0B396578D};; +FUCUS-YHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={76879C3D-8293-4C12-B873-26F59BCF54B9};Saatavuutta rajoitettu;{76879C3D-8293-4C12-B873-26F59BCF54B9};;;https://metadata.ymparisto.fi/dataset/;; +GOFREP-alue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B9487289-0C02-48E4-A26F-DAACDA60C462};Saatavuutta rajoitettu;{B9487289-0C02-48E4-A26F-DAACDA60C462};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Groundwater areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FA1742D1-8509-437C-846A-6637B3FF7345};Saatavuutta rajoitettu;{FA1742D1-8509-437C-846A-6637B3FF7345};;;https://metadata.ymparisto.fi/dataset/;; +HAURA- JA HAPSIKKAPOHJAT LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FF12F76-3162-4DD9-8320-668EDB9CBD9E};Saatavuutta rajoitettu;{6FF12F76-3162-4DD9-8320-668EDB9CBD9E};{C422872A-BA11-406C-9263-44D3302A7FCD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C422872A-BA11-406C-9263-44D3302A7FCD};; +Helsingin ihmispaineindeksi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={43163BBB-0D01-4375-BE28-33AA1607B2C3};Saatavuutta rajoitettu;{43163BBB-0D01-4375-BE28-33AA1607B2C3};{2063FB74-4879-4213-B9B5-E33FC848315E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2063FB74-4879-4213-B9B5-E33FC848315E};; +Historic Flood Maps;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FF5E8D4-5329-44D1-8741-D7F3223D0F30};Saatavuutta rajoitettu;{6FF5E8D4-5329-44D1-8741-D7F3223D0F30};;;https://metadata.ymparisto.fi/dataset/;; +HUB-biotooppiluokittelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7DB140D8-F133-42DB-B424-3309C2684A6D};Saatavuutta rajoitettu;{7DB140D8-F133-42DB-B424-3309C2684A6D};{E0001193-53A2-4CA3-B849-753621E51A69};Metsähallitus / Matti Sahla;https://metadata.ymparisto.fi/dataset/{E0001193-53A2-4CA3-B849-753621E51A69};; +Hydrologian ja vesien käytön tietojärjestelmä HYDRO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={86FC3188-6796-4C79-AC58-8DBC7B568827};Saatavuutta rajoitettu;{86FC3188-6796-4C79-AC58-8DBC7B568827};{86FC3188-6796-4C79-AC58-8DBC7B568827};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{86FC3188-6796-4C79-AC58-8DBC7B568827};; +Hätäkeskukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={51C9D60D-6D41-44BD-9136-C4933510DB2D};Saatavuutta rajoitettu;{51C9D60D-6D41-44BD-9136-C4933510DB2D};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Ilmapäästötietojärjestelmä IPTJ;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={033E574E-3B6D-4A40-A2BC-8C50C8FA86E0};Saatavuutta rajoitettu;{033E574E-3B6D-4A40-A2BC-8C50C8FA86E0};;;https://metadata.ymparisto.fi/dataset/;; +Jokisuistot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6DEF8642-7867-4047-AEB9-5134D24C4D64};Saatavuutta rajoitettu;{6DEF8642-7867-4047-AEB9-5134D24C4D64};{5B7F87B2-F082-4081-B653-3AEF12446C9E};Metsähallitus;https://metadata.ymparisto.fi/dataset/{5B7F87B2-F082-4081-B653-3AEF12446C9E};; +Järvirekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E4505311-A27A-4CD5-B207-89EFEEFE48CF};Saatavuutta rajoitettu;{E4505311-A27A-4CD5-B207-89EFEEFE48CF};{E4505311-A27A-4CD5-B207-89EFEEFE48CF};Suomen ympäristökeskus /Vesikeskus;https://metadata.ymparisto.fi/dataset/{E4505311-A27A-4CD5-B207-89EFEEFE48CF};; +Järvirekisteri / Säännöstelyhankkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};Saatavuutta rajoitettu;{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};; +Jätevedenpuhdistamot rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CE26C069-1F54-4F78-8FE2-A1C025E8C9E8};Saatavuutta rajoitettu;{CE26C069-1F54-4F78-8FE2-A1C025E8C9E8};{E1663280-9639-4559-B0EA-18D1B4BD5625};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{E1663280-9639-4559-B0EA-18D1B4BD5625};; +Kaavoituksen seurannan tilasto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4A29A502-D287-4956-AD84-8C467D1E4D40};Saatavuutta rajoitettu;{4A29A502-D287-4956-AD84-8C467D1E4D40};{4A29A502-D287-4956-AD84-8C467D1E4D40};Suomen ympäristökeskus/Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{4A29A502-D287-4956-AD84-8C467D1E4D40};; +Kalankasvatus ja verkkoaltaat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F4A6DADE-650B-4BA2-8E69-11ED37EC1335};Saatavuutta rajoitettu;{F4A6DADE-650B-4BA2-8E69-11ED37EC1335};{D2396FA4-DFF0-4D88-A647-70256C18941E};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{D2396FA4-DFF0-4D88-A647-70256C18941E};; +Kansainväliset jätesiirrot -rekisteri (Basel-rekisteri);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8D42F658-9DAD-4C65-9418-ABB00B92DF98};Saatavuutta rajoitettu;{8D42F658-9DAD-4C65-9418-ABB00B92DF98};{8D42F658-9DAD-4C65-9418-ABB00B92DF98};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8D42F658-9DAD-4C65-9418-ABB00B92DF98};; +Kansalliset kaupunkipuistot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={ECE5D7C1-FF3C-47C2-84F1-D878CD3DA6EE};Saatavuutta rajoitettu;{ECE5D7C1-FF3C-47C2-84F1-D878CD3DA6EE};{43E99E3C-7C91-42F0-A324-C9CB086E65DD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{43E99E3C-7C91-42F0-A324-C9CB086E65DD};; +Kasviplanktontietojärjestelmä KPLANK;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2F164F22-A4F9-468A-97E5-717B0B3D0E7B};Saatavuutta rajoitettu;{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};;https://metadata.ymparisto.fi/dataset/{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};; +Kemikaalien ympäristötietorekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={04B79E94-8D3B-451E-963B-19DDAD6F6215};Saatavuutta rajoitettu;{04B79E94-8D3B-451E-963B-19DDAD6F6215};;;https://metadata.ymparisto.fi/dataset/;; +Kertymärekisteri KERTY;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FF5FE8B1-B2BC-44C7-8A47-744F0A522CD2};Saatavuutta rajoitettu;{FF5FE8B1-B2BC-44C7-8A47-744F0A522CD2};;;https://metadata.ymparisto.fi/dataset/;; +Koekalastusrekisteri (Nordic-verkot, Coastal-verkot, sähkökoekalastus);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={348DFB97-5F9F-4902-9152-23B0C9505673};Saatavuutta rajoitettu;{348DFB97-5F9F-4902-9152-23B0C9505673};;Luonnonvarakeskus;https://metadata.ymparisto.fi/dataset/;; +Kuntien poikkeamispäätökset ja suunnittelutarveratkaisut sekä ELY-keskusten tekemät poikkeamispäätökset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0A81369B-6693-4BEB-80EF-238035480C3C};Saatavuutta rajoitettu;{0A81369B-6693-4BEB-80EF-238035480C3C};;;https://metadata.ymparisto.fi/dataset/;; +Laboratorion ja näytteenoton tiedonhallintajärjestelmä LIMS;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={939AE4E5-332A-4B0C-B9BF-96E0F16620A0};Saatavuutta rajoitettu;{939AE4E5-332A-4B0C-B9BF-96E0F16620A0};;;https://metadata.ymparisto.fi/dataset/;; +Lake and River Depth Profiles;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={23FB0660-5B2B-4212-BE02-772EB6E27E16};Saatavuutta rajoitettu;{23FB0660-5B2B-4212-BE02-772EB6E27E16};;;https://metadata.ymparisto.fi/dataset/;; +Laskeumarekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={19002DDF-91D0-48A3-8E68-891C8E7374D5};Saatavuutta rajoitettu;{19002DDF-91D0-48A3-8E68-891C8E7374D5};;;https://metadata.ymparisto.fi/dataset/;; +Liikennealueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F47A597F-FC14-47C5-BD3A-AE0710BC8A08};Saatavuutta rajoitettu;{F47A597F-FC14-47C5-BD3A-AE0710BC8A08};{22C3E0C5-3221-408E-B346-60493CCE6361};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{22C3E0C5-3221-408E-B346-60493CCE6361};; +Lumen peittämä ala Itämeren alueella 2007-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=Metadata_SYKE_SCA_Baltic_0.05deg_pv;Saatavuutta rajoitettu;Metadata_SYKE_SCA_Baltic_0.05deg_pv;;;https://metadata.ymparisto.fi/dataset/;; +Lumen peittämä ala Suomessa 2006-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=Metadata_SYKE_SCA_2006-2011_Finland;Saatavuutta rajoitettu;Metadata_SYKE_SCA_2006-2011_Finland;;;https://metadata.ymparisto.fi/dataset/;; +Maa-ainesluvat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={604DFF56-4157-46DE-84CB-71978DAC790A};Saatavuutta rajoitettu;{604DFF56-4157-46DE-84CB-71978DAC790A};{F163EC63-72A8-4FB5-A021-7A828BC1923F};KEHA-keskus;https://metadata.ymparisto.fi/dataset/{F163EC63-72A8-4FB5-A021-7A828BC1923F};; +Maaperän tilan tietojärjestelmä MATTI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};Saatavuutta rajoitettu;{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};Suomen ympäristökeskus ja ELY-keskukset;https://metadata.ymparisto.fi/dataset/{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};; +Maastoliikennerajoitukset-karttapalvelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E8222F77-00C4-4934-B2CB-0AB6A93D9E69};Saatavuutta rajoitettu;{E8222F77-00C4-4934-B2CB-0AB6A93D9E69};{8F7C8F06-B945-4D95-91BE-F0ECF1D76B7E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8F7C8F06-B945-4D95-91BE-F0ECF1D76B7E};; +Mahdolliset kapeat murtovesilahdet (1650);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8770CC62-F274-4700-9A81-592FBDE66635};Saatavuutta rajoitettu;{8770CC62-F274-4700-9A81-592FBDE66635};;;https://metadata.ymparisto.fi/dataset/;; +Mahdolliset laajat matalat lahdet (1160);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6655C0FA-9562-4015-B97A-0D07A19CF72C};Saatavuutta rajoitettu;{6655C0FA-9562-4015-B97A-0D07A19CF72C};;;https://metadata.ymparisto.fi/dataset/;; +Mahdolliset rannikon laguunit (1150);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={74345ACE-9D96-4A52-9E2F-7E8FE38C7809};Saatavuutta rajoitettu;{74345ACE-9D96-4A52-9E2F-7E8FE38C7809};;;https://metadata.ymparisto.fi/dataset/;; +MERENPOHJAN KIVIKKOISUUDEN TODENNÄKÖISYYS;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={41482CFB-D636-4396-AAB3-CBC82A28772A};Saatavuutta rajoitettu;{41482CFB-D636-4396-AAB3-CBC82A28772A};;;https://metadata.ymparisto.fi/dataset/;; +Merialuejako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3F43EA83-48C3-4D04-9DDF-67822FB32EF2};Saatavuutta rajoitettu;{3F43EA83-48C3-4D04-9DDF-67822FB32EF2};{6030C360-861F-4C16-B4BB-6A0EB4B5CF96};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6030C360-861F-4C16-B4BB-6A0EB4B5CF96};; +Merihiekan- ja soranottoalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={46D1A89C-7CE9-420E-B592-4018AC17EEE8};Saatavuutta rajoitettu;{46D1A89C-7CE9-420E-B592-4018AC17EEE8};{E35DE705-2986-43F2-8678-F219F1D097F2};Metsähallitus;https://metadata.ymparisto.fi/dataset/{E35DE705-2986-43F2-8678-F219F1D097F2};; +Meripelastusvastuualue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A2C05340-3276-4FBB-8350-607560E3C31F};Saatavuutta rajoitettu;{A2C05340-3276-4FBB-8350-607560E3C31F};{22A9AD4A-E725-4426-BD1A-916A23CF73BE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{22A9AD4A-E725-4426-BD1A-916A23CF73BE};; +MERIVEDEN LÄMPÖTILA POHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={ED3D3E91-8674-4532-8E78-3E63950A9BE5};Saatavuutta rajoitettu;{ED3D3E91-8674-4532-8E78-3E63950A9BE5};{8E562E41-5465-40A1-9990-4EACE2537F01};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8E562E41-5465-40A1-9990-4EACE2537F01};; +Meriveden suolaisuus pinnalla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EA4227EC-6958-4EC1-AE3C-3051FB9678B8};Saatavuutta rajoitettu;{EA4227EC-6958-4EC1-AE3C-3051FB9678B8};{D9F7BA00-14B9-40A0-9FEB-A9549266D7FF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D9F7BA00-14B9-40A0-9FEB-A9549266D7FF};; +MERIVEDEN SUOLAISUUS POHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7E5A9C8-08A2-4272-9AB2-ADA81A2E6C69};Saatavuutta rajoitettu;{B7E5A9C8-08A2-4272-9AB2-ADA81A2E6C69};;;https://metadata.ymparisto.fi/dataset/;; +Mire vegetation zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={79F1A09C-54C6-4EBE-96D8-1970459896EE};Saatavuutta rajoitettu;{79F1A09C-54C6-4EBE-96D8-1970459896EE};;;https://metadata.ymparisto.fi/dataset/;; +MRL:n mukaisten yleiskaavojen ulkorajaukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D8DD3305-0313-47F4-B241-71658B6C06AC};Saatavuutta rajoitettu;{D8DD3305-0313-47F4-B241-71658B6C06AC};;;https://metadata.ymparisto.fi/dataset/;; +MYTILUS TROSSULUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FECA3DEC-6C77-4424-BC42-0AE1452B2BA0};Saatavuutta rajoitettu;{FECA3DEC-6C77-4424-BC42-0AE1452B2BA0};{6AC94879-B22D-42DA-B3F5-3FC955AE03E4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6AC94879-B22D-42DA-B3F5-3FC955AE03E4};; +NAJAS MARINA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6906D7D5-BAEC-4006-8FDB-84732B61ADE7};Saatavuutta rajoitettu;{6906D7D5-BAEC-4006-8FDB-84732B61ADE7};{61E89CB2-03BA-4FD5-A231-BA2D909D30EF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{61E89CB2-03BA-4FD5-A231-BA2D909D30EF};; +Nannut-luokittelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8A4E6487-B27A-4F3B-8EA4-4651EA39ACCE};Saatavuutta rajoitettu;{8A4E6487-B27A-4F3B-8EA4-4651EA39ACCE};{F60E6BF7-9BC6-476D-8102-2E310A204BE0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F60E6BF7-9BC6-476D-8102-2E310A204BE0};; +National Database of Regional Land Use Plans;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2FAA5D6B-C053-465E-812C-119798581F5C};Saatavuutta rajoitettu;{2FAA5D6B-C053-465E-812C-119798581F5C};;;https://metadata.ymparisto.fi/dataset/;; +National Landscape Division;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={09F5613F-D9A2-4C68-818D-70B018437888};Saatavuutta rajoitettu;{09F5613F-D9A2-4C68-818D-70B018437888};;;https://metadata.ymparisto.fi/dataset/;; +Nationally Designated Nature Protected Areas and Wilderness Reserves;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9871C541-9E84-4241-A1E5-51C596A5A4E2};Saatavuutta rajoitettu;{9871C541-9E84-4241-A1E5-51C596A5A4E2};;;https://metadata.ymparisto.fi/dataset/;; +Nationally Valuable Aeolian and Littoral Deposits;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={04399CD4-86CD-453E-B683-5CFF53FA36D0};Saatavuutta rajoitettu;{04399CD4-86CD-453E-B683-5CFF53FA36D0};;;https://metadata.ymparisto.fi/dataset/;; +Nationally Valuable Moraine Formations;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0193799E-1F36-4FB9-A0B9-48264860C58C};Saatavuutta rajoitettu;{0193799E-1F36-4FB9-A0B9-48264860C58C};;;https://metadata.ymparisto.fi/dataset/;; +Nationally Valuable Rocky Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2753EE72-1F06-4291-987F-08FA693ED5CA};Saatavuutta rajoitettu;{2753EE72-1F06-4291-987F-08FA693ED5CA};;;https://metadata.ymparisto.fi/dataset/;; +Natura 2000 -tietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7AC30A3E-8E38-4717-8D2E-80585623212B};Saatavuutta rajoitettu;{7AC30A3E-8E38-4717-8D2E-80585623212B};;Suomen ympäristökeskus / Luontoympäristökeskus / Biodiversiteettiyksikkö;https://metadata.ymparisto.fi/dataset/;; +Nature Recreational Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9F0B1FD-C41E-46A1-BA30-E9A29DF12D70};Saatavuutta rajoitettu;{E9F0B1FD-C41E-46A1-BA30-E9A29DF12D70};;;https://metadata.ymparisto.fi/dataset/;; +NÄKÖSYVYYS (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={55AB842F-9CED-4E80-A7E5-07A54F0AE4A4};Saatavuutta rajoitettu;{55AB842F-9CED-4E80-A7E5-07A54F0AE4A4};{811B434E-75A1-4F53-9445-A4B4BDADA499};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{811B434E-75A1-4F53-9445-A4B4BDADA499};; +Olemassa oleva ja suunniteltu tuulivoima;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9B856E2E-6F8F-4B9A-8618-9923F238852D};Saatavuutta rajoitettu;{9B856E2E-6F8F-4B9A-8618-9923F238852D};{F690781C-D187-4611-8D80-A703804F7B59};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F690781C-D187-4611-8D80-A703804F7B59};; +Oppilaitokset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7753FF3C-D1E8-453A-B3F1-8412A2AD1CD1};Saatavuutta rajoitettu;{7753FF3C-D1E8-453A-B3F1-8412A2AD1CD1};;Tilastokeskus;https://metadata.ymparisto.fi/dataset/;; +Outlines of Local Master Plans Ratified in Accordance to the old Building Act;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6771A0AD-3B9C-450E-9536-0B036B47A662};Saatavuutta rajoitettu;{6771A0AD-3B9C-450E-9536-0B036B47A662};;;https://metadata.ymparisto.fi/dataset/;; +Outlines of Local Master Plans Ratified in Accordance with the Landuse and Building Act;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2D70E79B-36F8-4E5E-BB6C-0DD7C6E5639D};Saatavuutta rajoitettu;{2D70E79B-36F8-4E5E-BB6C-0DD7C6E5639D};;;https://metadata.ymparisto.fi/dataset/;; +Paikkatietoaineistojen latauspalvelu LAPIO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DDFEA0F7-7A42-4E8E-9782-57DD74697BD1};Saatavuutta rajoitettu;{DDFEA0F7-7A42-4E8E-9782-57DD74697BD1};{3EEF491C-53D4-4614-B64C-1D98A699198C};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3EEF491C-53D4-4614-B64C-1D98A699198C};; +Paikkatietoanalyysien tuloksia;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BBDCFF54-8109-4795-8BB5-15FA9480BB9C};Saatavuutta rajoitettu;{BBDCFF54-8109-4795-8BB5-15FA9480BB9C};{F039C4B7-7D3C-4EFA-99B0-C52091BC97B4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F039C4B7-7D3C-4EFA-99B0-C52091BC97B4};; +Patoturvallisuuden tietojärjestelmä PATOTURVA;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={895902E7-031C-48AD-A8AC-DCE7D325B9EE};Saatavuutta rajoitettu;{895902E7-031C-48AD-A8AC-DCE7D325B9EE};;;https://metadata.ymparisto.fi/dataset/;; +Pelastustoimen alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={75182F9D-D7D5-4A3A-8556-1C96577F0A52};Saatavuutta rajoitettu;{75182F9D-D7D5-4A3A-8556-1C96577F0A52};{34A6DAEE-DE66-45A2-B3A1-CB23CF6FF45E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{34A6DAEE-DE66-45A2-B3A1-CB23CF6FF45E};; +PINNAN AALTOEKSPOSITIO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB655D7F-1E25-4E19-AE7F-5DEBF1AEFC6C};Saatavuutta rajoitettu;{BB655D7F-1E25-4E19-AE7F-5DEBF1AEFC6C};{216DD75B-59A8-408A-80CD-F0785E548769};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{216DD75B-59A8-408A-80CD-F0785E548769};; +Pintavesien tilan tietojärjestelmä, vedenlaatu PIVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B1444E19-0F36-49F5-A849-01A3D2083A11};Saatavuutta rajoitettu;{B1444E19-0F36-49F5-A849-01A3D2083A11};;;https://metadata.ymparisto.fi/dataset/;; +Pohjaeläinhavaintoja POHJE-tietokannasta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2FA13650-7CB3-4299-8663-7542769D102E};Saatavuutta rajoitettu;{2FA13650-7CB3-4299-8663-7542769D102E};{0A22C866-3D0C-476A-9957-9275EB18B8AD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0A22C866-3D0C-476A-9957-9275EB18B8AD};; +Pohjaeläintietojärjestelmä - POHJE;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};Saatavuutta rajoitettu;{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};;https://metadata.ymparisto.fi/dataset/{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};; +Pohjavesitietojärjestelmä POVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C594F22E-B642-434D-B6F2-6ECB03F760C7};Saatavuutta rajoitettu;{C594F22E-B642-434D-B6F2-6ECB03F760C7};{C594F22E-B642-434D-B6F2-6ECB03F760C7};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C594F22E-B642-434D-B6F2-6ECB03F760C7};; +Poronhoidon paikkatiedot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={12E7100A-C010-40A3-A441-4EED54C9FC64};Saatavuutta rajoitettu;{12E7100A-C010-40A3-A441-4EED54C9FC64};{A6E6BD51-E463-4E0A-88EC-71DBFDF2223E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A6E6BD51-E463-4E0A-88EC-71DBFDF2223E};; +POTAMOGETON PERFOLIATUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D7F3685D-0432-4FC3-B0D4-13E798050A67};Saatavuutta rajoitettu;{D7F3685D-0432-4FC3-B0D4-13E798050A67};{8FD8F68C-373B-4A47-BB76-2E9040F3C488};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8FD8F68C-373B-4A47-BB76-2E9040F3C488};; +Protected Rapids;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9540CC1E-3E98-4C3D-A214-DC26A7DE6953};Saatavuutta rajoitettu;{9540CC1E-3E98-4C3D-A214-DC26A7DE6953};;;https://metadata.ymparisto.fi/dataset/;; +PUNALEVÄYHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={179A08D0-3B37-4A80-837E-83F151D5CDEA};Saatavuutta rajoitettu;{179A08D0-3B37-4A80-837E-83F151D5CDEA};;;https://metadata.ymparisto.fi/dataset/;; +Puolustusvoimien suoja-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={61D3FA7B-D8AC-40BA-A552-9FCED8F87375};Saatavuutta rajoitettu;{61D3FA7B-D8AC-40BA-A552-9FCED8F87375};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +PYLAIELLA LITTORALIS & ECTOCARPUS SILICULOSUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CF4921DE-7CD1-48C6-8C9C-1BFFC6662E14};Saatavuutta rajoitettu;{CF4921DE-7CD1-48C6-8C9C-1BFFC6662E14};{955901B1-C6D0-45CA-8EA6-C1EF95CA1C50};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{955901B1-C6D0-45CA-8EA6-C1EF95CA1C50};; +Rakennusperinnön hoitoavustushakemukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9D206B64-2EB7-47CC-AC9C-48459242444E};Saatavuutta rajoitettu;{9D206B64-2EB7-47CC-AC9C-48459242444E};{2CAEA52F-82FA-4E96-8E79-1327F27EF9A9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2CAEA52F-82FA-4E96-8E79-1327F27EF9A9};; +Rakennusperinnön inventointi kohteet ja alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4B84256C-0F01-456E-8A41-63231FC0D8F5};Saatavuutta rajoitettu;{4B84256C-0F01-456E-8A41-63231FC0D8F5};{D653718F-EE09-4F08-95B5-2AC42BAD5337};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D653718F-EE09-4F08-95B5-2AC42BAD5337};; +Rantalohkojako (SÖKÖ-toimintamalli);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={11A39DCD-4A35-4B90-AFBD-EEC3B778378C};Saatavuutta rajoitettu;{11A39DCD-4A35-4B90-AFBD-EEC3B778378C};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +River Basin Districts;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={65C4C67D-94F6-4B52-A656-069CC5853778};Saatavuutta rajoitettu;{65C4C67D-94F6-4B52-A656-069CC5853778};;;https://metadata.ymparisto.fi/dataset/;; +Ruoppaukset rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1A50B09C-2DEF-46C0-8664-19A3A5BF89CE};Saatavuutta rajoitettu;{1A50B09C-2DEF-46C0-8664-19A3A5BF89CE};{911BBDBD-5371-44E5-8AEA-3DEE2248AEE6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{911BBDBD-5371-44E5-8AEA-3DEE2248AEE6};; +Saaristomeren ja Selkämeren fladat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={45E434FE-36DF-431C-A9F2-D65C433F8751};Saatavuutta rajoitettu;{45E434FE-36DF-431C-A9F2-D65C433F8751};{FC7C4F23-81E4-4037-9DED-E45575205D30};Turun ammattikorkeakoulu;https://metadata.ymparisto.fi/dataset/{FC7C4F23-81E4-4037-9DED-E45575205D30};; +Sea Region Division;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={313F5784-71A0-46C2-A5B2-86AD9B17A4E8};Saatavuutta rajoitettu;{313F5784-71A0-46C2-A5B2-86AD9B17A4E8};;;https://metadata.ymparisto.fi/dataset/;; +Shoreline10 (Ranta10) and River network;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CED8AC3C-0651-4117-88A6-44CD05F6B42D};Saatavuutta rajoitettu;{CED8AC3C-0651-4117-88A6-44CD05F6B42D};;;https://metadata.ymparisto.fi/dataset/;; +SINISIMPUKKAYHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2F7CC7D6-380E-4E77-834F-7682EC6BC4FF};Saatavuutta rajoitettu;{2F7CC7D6-380E-4E77-834F-7682EC6BC4FF};;;https://metadata.ymparisto.fi/dataset/;; +STUCKENIA FILIFORMIS & STUCKENIA PECTINATA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BD151356-6EDB-4CB2-9BEF-E72D5AEBE936};Saatavuutta rajoitettu;{BD151356-6EDB-4CB2-9BEF-E72D5AEBE936};{BC774445-3D76-4F4F-B6A4-A63C631F0BAE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BC774445-3D76-4F4F-B6A4-A63C631F0BAE};; +Sukelluksissa havaittujen makroleväsukujen otannalla korjattu määrä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FF890247-EDD4-45E2-97BE-4570039870B7};Saatavuutta rajoitettu;{FF890247-EDD4-45E2-97BE-4570039870B7};{FBE17630-F6E1-47EA-B07E-C4A29BC33E3D};Metsähallitus;https://metadata.ymparisto.fi/dataset/{FBE17630-F6E1-47EA-B07E-C4A29BC33E3D};; +SYKEn hakemistorajapinta (OData);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3563384E-DF21-49F6-9E0A-CCB1B8A74A44};Saatavuutta rajoitettu;{3563384E-DF21-49F6-9E0A-CCB1B8A74A44};{318D5E0E-7E13-47F2-93E7-04DFE2A12917};Suomen ympäristökeskus / Tietokeskus;https://metadata.ymparisto.fi/dataset/{318D5E0E-7E13-47F2-93E7-04DFE2A12917};; +Säähavaintoasemat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D2BF7AA2-6E0A-4EFF-BB77-1EA5330A3518};Saatavuutta rajoitettu;{D2BF7AA2-6E0A-4EFF-BB77-1EA5330A3518};{CDE7624A-AA3A-4592-B086-24008F05108D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CDE7624A-AA3A-4592-B086-24008F05108D};; +SÖKÖ-karttalehdet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3A7B5ECF-5A1A-4CE8-8880-EFA161B0452E};Saatavuutta rajoitettu;{3A7B5ECF-5A1A-4CE8-8880-EFA161B0452E};{D925EC77-C5F0-4D1E-A691-93ED0FACDAD6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D925EC77-C5F0-4D1E-A691-93ED0FACDAD6};; +Teollisuus rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BDBE0944-5B2C-4CE8-A404-34B8292DF808};Saatavuutta rajoitettu;{BDBE0944-5B2C-4CE8-A404-34B8292DF808};{8D033A6C-D9A7-4622-B518-E4358303BD2C};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{8D033A6C-D9A7-4622-B518-E4358303BD2C};; +Tieriskirekisteri TSRR;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8047A089-C13F-4729-920B-3C18F471F711};Saatavuutta rajoitettu;{8047A089-C13F-4729-920B-3C18F471F711};{47A9B147-CC81-460F-976B-E3A2A756D15A};Pohjavesiaineisto- SYKE, Tieaineisto- LiV;https://metadata.ymparisto.fi/dataset/{47A9B147-CC81-460F-976B-E3A2A756D15A};; +Total Suspended Matter, FI-LAKES, 2012-06-20;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=e98cabc7-65ad-4e72-a6d6-f42e2cc591da;Saatavuutta rajoitettu;e98cabc7-65ad-4e72-a6d6-f42e2cc591da;;;https://metadata.ymparisto.fi/dataset/;; +Total suspended matter, FI-SOUTH, 2009-09-15;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=69b7df80-3d67-11e2-941e-005056c00008;Saatavuutta rajoitettu;69b7df80-3d67-11e2-941e-005056c00008;;;https://metadata.ymparisto.fi/dataset/;; +Traffic and Water Ways' Restriction Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A8126E3C-8F96-43C3-883D-BF315E38284E};Saatavuutta rajoitettu;{A8126E3C-8F96-43C3-883D-BF315E38284E};;;https://metadata.ymparisto.fi/dataset/;; +Traffic Risk Zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F1E7CB96-6CA3-4E51-B057-464AF658C46E};Saatavuutta rajoitettu;{F1E7CB96-6CA3-4E51-B057-464AF658C46E};;;https://metadata.ymparisto.fi/dataset/;; +Tulvariskiruudut;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7F0ABF2F-A175-4D9A-A04B-C6514B94D082};Saatavuutta rajoitettu;{7F0ABF2F-A175-4D9A-A04B-C6514B94D082};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Tulvatietojärjestelmä TULVATJ;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={58AAFEFC-1429-4343-8EE2-F5E136E0A276};Saatavuutta rajoitettu;{58AAFEFC-1429-4343-8EE2-F5E136E0A276};{58AAFEFC-1429-4343-8EE2-F5E136E0A276};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{58AAFEFC-1429-4343-8EE2-F5E136E0A276};; +Tuottajavastuutietojärjestelmä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0E73BB28-1574-48A0-87CA-FD4BC1211896};Saatavuutta rajoitettu;{0E73BB28-1574-48A0-87CA-FD4BC1211896};;;https://metadata.ymparisto.fi/dataset/;; +Uomatietojärjestelmä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FB936A19-421D-486D-95AC-009BD53AA1B2};Saatavuutta rajoitettu;{FB936A19-421D-486D-95AC-009BD53AA1B2};{FB936A19-421D-486D-95AC-009BD53AA1B2};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{FB936A19-421D-486D-95AC-009BD53AA1B2};; +Valtakunnallinen maakuntakaavapaikkatietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8E578155-1FD3-4B69-B6EB-0AB18B8F6FE6};Saatavuutta rajoitettu;{8E578155-1FD3-4B69-B6EB-0AB18B8F6FE6};{82174E6B-DBC6-439F-A0F0-B425C8FBB28F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{82174E6B-DBC6-439F-A0F0-B425C8FBB28F};; +Valtakunnallisen yöperhosseurannan keskustietokanta YÖPETI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1FF762A2-EE3F-415C-9895-D866B2E8A9CF};Saatavuutta rajoitettu;{1FF762A2-EE3F-415C-9895-D866B2E8A9CF};{4CA7BB48-01F0-4544-B153-E17569155FBD};SYKE;https://metadata.ymparisto.fi/dataset/{4CA7BB48-01F0-4544-B153-E17569155FBD};; +Valtion öljyntorjuntavarastot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3E35CBD1-1C57-4E17-96EC-6E41256E4619};Saatavuutta rajoitettu;{3E35CBD1-1C57-4E17-96EC-6E41256E4619};{872D9B86-1902-4343-999C-2F76348C4524};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{872D9B86-1902-4343-999C-2F76348C4524};; +Value-valuma-aluejako (ehdotus);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={31B11BD6-7886-447C-8A24-2B8FFE6B2D07};Saatavuutta rajoitettu;{31B11BD6-7886-447C-8A24-2B8FFE6B2D07};{E848DF66-C3A7-47E5-956A-5FEBBDBD924B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E848DF66-C3A7-47E5-956A-5FEBBDBD924B};; +Valvonta ja kuormitustietojärjestelmä VAHTI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D1539BE1-7A48-4CC8-AEC9-B32C7650A399};Saatavuutta rajoitettu;{D1539BE1-7A48-4CC8-AEC9-B32C7650A399};;;https://metadata.ymparisto.fi/dataset/;; +Vanhan rakennuslain mukaisesti vahvistettujen yleiskaavojen rasterit;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1377662D-F6F1-48F7-86A0-ADD001F08CE1};Saatavuutta rajoitettu;{1377662D-F6F1-48F7-86A0-ADD001F08CE1};{970599E7-8793-47A6-BF67-23E845E82ED1};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{970599E7-8793-47A6-BF67-23E845E82ED1};; +Vanhan rakennuslain mukaisesti vahvistettujen yleiskaavojen ulkorajaukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E200F2D-7983-42C1-84AF-459FD31AB4EC};Saatavuutta rajoitettu;{1E200F2D-7983-42C1-84AF-459FD31AB4EC};{AAFD1646-16FC-42D8-9F1B-DA8272A9D062};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AAFD1646-16FC-42D8-9F1B-DA8272A9D062};; +Varsinais-Suomen veneluiskat 2016;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AFB2C26E-E63B-4324-8E06-3B806EC37C92};Saatavuutta rajoitettu;{AFB2C26E-E63B-4324-8E06-3B806EC37C92};{5E45B067-25E6-41F2-8129-DD7C2BF67148};Turun ammattikorkeakoulu Oy;https://metadata.ymparisto.fi/dataset/{5E45B067-25E6-41F2-8129-DD7C2BF67148};; +Varsinais-Suomen väylien riskikartoitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={00DEEA25-2286-49B4-AED4-CB20A1BA5B9E};Saatavuutta rajoitettu;{00DEEA25-2286-49B4-AED4-CB20A1BA5B9E};{AB829531-5139-4A9D-9CF6-162AE62B27D0};Turun ammattikorkeakoulu;https://metadata.ymparisto.fi/dataset/{AB829531-5139-4A9D-9CF6-162AE62B27D0};; +VELMU kartoitusmenetelmät;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7F62E2AC-7C1B-4B3F-A66A-8BE03E01388B};Saatavuutta rajoitettu;{7F62E2AC-7C1B-4B3F-A66A-8BE03E01388B};{2BED2EB3-CDAA-4071-ADD3-1FC081C23440};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2BED2EB3-CDAA-4071-ADD3-1FC081C23440};; +VELMU kuvapisteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FBE41DF9-F410-4490-B9B6-8418CB33617E};Saatavuutta rajoitettu;{FBE41DF9-F410-4490-B9B6-8418CB33617E};{4504165A-74A4-4675-B95A-FA5274C4984A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{4504165A-74A4-4675-B95A-FA5274C4984A};; +VELMU lajihavainnot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D03358DD-CA34-487F-8F85-03A74C9D7296};Saatavuutta rajoitettu;{D03358DD-CA34-487F-8F85-03A74C9D7296};{C3C7C925-5872-4C30-A824-8F723BE3ABC8};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C3C7C925-5872-4C30-A824-8F723BE3ABC8};; +VELMU lajihavainnot (uhanalaiset lajit);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F500A951-A177-4CAA-83C5-FC54F2255FAB};Saatavuutta rajoitettu;{F500A951-A177-4CAA-83C5-FC54F2255FAB};{BF30DFF5-B5F8-4D8D-BAF7-D3671BBE432A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF30DFF5-B5F8-4D8D-BAF7-D3671BBE432A};; +Velmu taustakartta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B2B2A595-1225-4E18-B49C-EEBB5A6C5CE3};Saatavuutta rajoitettu;{B2B2A595-1225-4E18-B49C-EEBB5A6C5CE3};{60776441-5F9D-43EB-B902-A30CFBE89C2A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{60776441-5F9D-43EB-B902-A30CFBE89C2A};; +Venesatamat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4166A97E-E513-4B69-B498-447A8FA71B9A};Saatavuutta rajoitettu;{4166A97E-E513-4B69-B498-447A8FA71B9A};{D4C89F91-AA18-4359-B1D6-42C9C436332A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D4C89F91-AA18-4359-B1D6-42C9C436332A};; +Vesienhoitoalueiden pintavesien kaikki seurantapaikat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={356F0345-ABFE-4245-8231-BE4BFA83C541};Saatavuutta rajoitettu;{356F0345-ABFE-4245-8231-BE4BFA83C541};{79CF5A64-2690-4BF1-83C9-66C9CF8C041D};Suomen ympäristökeskus/Vesikeskus;https://metadata.ymparisto.fi/dataset/{79CF5A64-2690-4BF1-83C9-66C9CF8C041D};; +Vesienhoitoalueiden seuranta - VHS Seuranta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F2FAD430-3632-4ACC-B2B1-70680795361E};Saatavuutta rajoitettu;{F2FAD430-3632-4ACC-B2B1-70680795361E};{F2FAD430-3632-4ACC-B2B1-70680795361E};Suomen ympäristökeskus / Vesikeskus / Sisävesiyksikkö;https://metadata.ymparisto.fi/dataset/{F2FAD430-3632-4ACC-B2B1-70680795361E};; +vesienhoitoalueiden seurantapaikat, Pohjavesi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3E07BDDC-AB68-4CDB-A647-BA6FCA784AB3};Saatavuutta rajoitettu;{3E07BDDC-AB68-4CDB-A647-BA6FCA784AB3};{5168AC0A-DD96-4231-9B4A-26DEAC055DC4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5168AC0A-DD96-4231-9B4A-26DEAC055DC4};; +Vesienhoitoalueiden seurantapaikat. Pintavesi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={22E6CAF3-20A9-44D1-B054-E019FEF882D4};Saatavuutta rajoitettu;{22E6CAF3-20A9-44D1-B054-E019FEF882D4};;;https://metadata.ymparisto.fi/dataset/;; +Vesihuollon tietojärjestelmä VEETI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F363ACA2-6CF5-46EF-907C-8F6CAF50C17B};Saatavuutta rajoitettu;{F363ACA2-6CF5-46EF-907C-8F6CAF50C17B};{373B7BEC-F23D-4AEF-9194-A646006F95AD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{373B7BEC-F23D-4AEF-9194-A646006F95AD};; +Vesihuoltolaitosten tilastointijärjestelmä VELVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DA001EC6-DB26-4339-AB97-DAB8FC2968B3};Saatavuutta rajoitettu;{DA001EC6-DB26-4339-AB97-DAB8FC2968B3};;;https://metadata.ymparisto.fi/dataset/;; +Vesihuoltoverkoston potentiaali (jätevesi, talousvesi ja jätevesi);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={479C025B-3ECC-4E97-8FF0-23E82B5A521B};Saatavuutta rajoitettu;{479C025B-3ECC-4E97-8FF0-23E82B5A521B};{363584D7-C854-4A20-BD7E-A1385B4E171D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{363584D7-C854-4A20-BD7E-A1385B4E171D};; +Vesiliikennerajoitukset-karttapalvelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2AD52986-F67E-4417-B733-BF57C94B4F02};Saatavuutta rajoitettu;{2AD52986-F67E-4417-B733-BF57C94B4F02};{5E05F2A7-17C5-44D3-99E2-68C263D3DD32};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5E05F2A7-17C5-44D3-99E2-68C263D3DD32};; +Vesimuodostumat-tietojärjestelmä VEMU;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6CD780AB-790D-4DA8-8962-9C8186B4127D};Saatavuutta rajoitettu;{6CD780AB-790D-4DA8-8962-9C8186B4127D};{6CD780AB-790D-4DA8-8962-9C8186B4127D};Suomen ympäristökeskus / Vesikeskus / Vesivarayksikkö;https://metadata.ymparisto.fi/dataset/{6CD780AB-790D-4DA8-8962-9C8186B4127D};; +Vesistömallijärjestelmä (WSFS-VEMALA);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C08C0416-6605-4BC5-8956-58AAF95F0030};Saatavuutta rajoitettu;{C08C0416-6605-4BC5-8956-58AAF95F0030};;;https://metadata.ymparisto.fi/dataset/;; +Vesistötyöt VESTY - Vesistöhankkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CA4578AC-3B2E-488F-8A0D-C91E470DAE49};Saatavuutta rajoitettu;{CA4578AC-3B2E-488F-8A0D-C91E470DAE49};{789B889E-6D94-413F-BD07-F811137637B7};Lasse Järvenpää;https://metadata.ymparisto.fi/dataset/{789B889E-6D94-413F-BD07-F811137637B7};; +Vesistötyöt VESTY - Rakenteet ja toimenpiteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={54CB6655-3038-4FAC-9822-5BADA622570F};Saatavuutta rajoitettu;{54CB6655-3038-4FAC-9822-5BADA622570F};{C13F44E5-E626-4AE5-BD23-EDCC1A21CDA9};Suomen ympäristökeskus / Vesikeskus / Vesivarayksikkö;https://metadata.ymparisto.fi/dataset/{C13F44E5-E626-4AE5-BD23-EDCC1A21CDA9};; +Vesivarat - käyttötoiminta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6F7ADB6E-193B-47A2-A4F6-2320D66D706A};Saatavuutta rajoitettu;{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};; +Vesiviljelyn sijainninohjaussuunnitelma;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9B1F82F-D696-49A1-9CC8-B69EEE5D61FF};Saatavuutta rajoitettu;{E9B1F82F-D696-49A1-9CC8-B69EEE5D61FF};;Maa- ja metsätalousministeriö, Ruokavirasto;https://metadata.ymparisto.fi/dataset/;; +Väestötietojärjestelmän rakennus- ja huoneistotiedot RHR;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={18C8309E-3963-4738-B6D2-96BA825344DC};Saatavuutta rajoitettu;{18C8309E-3963-4738-B6D2-96BA825344DC};;Digi- ja väestötietovirasto;https://metadata.ymparisto.fi/dataset/;; +Vähittäiskaupan toimipaikat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2A5BD858-ECAB-4E8B-B3B1-7FD0FE0DA798};Saatavuutta rajoitettu;{2A5BD858-ECAB-4E8B-B3B1-7FD0FE0DA798};;Tilastokeskus;https://metadata.ymparisto.fi/dataset/;; +Yleiskaavarekisteri - Yleiskaava MOPO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={34E2147E-D704-408D-A053-F91EEE20F94F};Saatavuutta rajoitettu;{34E2147E-D704-408D-A053-F91EEE20F94F};;;https://metadata.ymparisto.fi/dataset/;; +Ympäristöhallinnon kirjastojen kokoelma- ja artikkelitietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={51D1BC1D-93E5-4369-8019-630ACDEF53BA};Saatavuutta rajoitettu;{51D1BC1D-93E5-4369-8019-630ACDEF53BA};{51D1BC1D-93E5-4369-8019-630ACDEF53BA};Suomen ympäristökeskus / Tietokeskus / Tietopalvelu ja kirjasto;https://metadata.ymparisto.fi/dataset/{51D1BC1D-93E5-4369-8019-630ACDEF53BA};; +Ympäristöhallinnon kirjastojen lehtitietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B313B6FD-1CAB-41DD-9EF8-CF602F361773};Saatavuutta rajoitettu;{B313B6FD-1CAB-41DD-9EF8-CF602F361773};{B313B6FD-1CAB-41DD-9EF8-CF602F361773};Suomen ympäristökeskus / Tietokeskus / Tietopalvelu ja kirjasto;https://metadata.ymparisto.fi/dataset/{B313B6FD-1CAB-41DD-9EF8-CF602F361773};; +Ympäristökarttapalvelu karpalo;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7FB975C-846E-451A-9F5E-00EE3D2383AA};Saatavuutta rajoitettu;{B7FB975C-846E-451A-9F5E-00EE3D2383AA};{1334BB17-D27D-4876-9362-EF59256CC6BD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{1334BB17-D27D-4876-9362-EF59256CC6BD};; +Zostera marina levinneisyyysmalli;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CA32C9F7-7475-4EFE-8E1C-22B1FB96F008};Saatavuutta rajoitettu;{CA32C9F7-7475-4EFE-8E1C-22B1FB96F008};{30F193DB-2CD7-4DC1-8A31-84CB06B16213};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{30F193DB-2CD7-4DC1-8A31-84CB06B16213};; \ No newline at end of file From d3b578bad11be667a7cdcc3b737372679828fd2d Mon Sep 17 00:00:00 2001 From: Katri Tegel Date: Thu, 15 Oct 2020 14:42:05 +0300 Subject: [PATCH 04/21] Revert "CSCFAIRMETA-762-New-links-for-SYKE-datasets (#644)" This reverts commit ff4cb8777f139bd29d8c4bd321f169dba076fa90. --- .../api/oaipmh/base/metax_oai_server.py | 3 +- .../migrations/0027_auto_20201013_1404.py | 56 ---- .../migrations/datasets/SYKE_csv.csv | 240 ------------------ 3 files changed, 1 insertion(+), 298 deletions(-) delete mode 100644 src/metax_api/migrations/0027_auto_20201013_1404.py delete mode 100644 src/metax_api/migrations/datasets/SYKE_csv.csv diff --git a/src/metax_api/api/oaipmh/base/metax_oai_server.py b/src/metax_api/api/oaipmh/base/metax_oai_server.py index a4f99ecf..89f3e6c0 100644 --- a/src/metax_api/api/oaipmh/base/metax_oai_server.py +++ b/src/metax_api/api/oaipmh/base/metax_oai_server.py @@ -17,8 +17,7 @@ from metax_api.services import CatalogRecordService as CRS from metax_api.services.datacite_service import DataciteException, convert_cr_to_datacite_cr_json -# SYKE_IDENTIFIER_PREFIX -SYKE_URL_PREFIX_TEMPLATE = 'https://metadata.ymparisto.fi/dataset/%s' +SYKE_URL_PREFIX_TEMPLATE = 'http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=%s' DATACATALOGS_SET = 'datacatalogs' DATASETS_SET = 'datasets' OAI_DC_MDPREFIX = 'oai_dc' diff --git a/src/metax_api/migrations/0027_auto_20201013_1404.py b/src/metax_api/migrations/0027_auto_20201013_1404.py deleted file mode 100644 index 5719b0b4..00000000 --- a/src/metax_api/migrations/0027_auto_20201013_1404.py +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by Django 3.0.7 on 2020-10-13 11:04 - -from django.db import migrations -import csv -import json -import logging - -logger = logging.getLogger(__name__) - -def rename(apps, schema_editor): - CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') - - # Loop through csv - with open('metax_api/migrations/datasets/SYKE_csv.csv', newline='') as csvfile: - reader = csv.DictReader(csvfile, delimiter=";", quotechar='"') - - for row in reader: - old_guid = row["vanha guid"] - - if old_guid == 'vanha guid': # Check if is heade value - pass - else: - # Get dataset - try: - cr = CatalogRecord.objects.get(research_dataset__other_identifier__0__notation = old_guid) - new_guid = row["uusi guid"] - cr.research_dataset['other_identifier'][0]['notation'] = new_guid - cr.research_dataset['other_identifier'][0]['old_notation'] = old_guid - cr.user_modified = "migration_0027" - cr.save() - logger.info(f"successful migration of cr {cr.id} with new guid {new_guid} and old guid {old_guid}") - except CatalogRecord.DoesNotExist: - pass - -def revert(apps, schema_editor): - CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') - modified = CatalogRecord.objects.filter(user_modified="migration_0027") - for cr in modified: - try: - o_ids = cr.research_dataset['other_identifier'] - if "old_notation" in o_ids[0].keys(): - cr.research_dataset['other_identifier'][0]['notation'] = o_ids[0]["old_notation"] - del cr.research_dataset['other_identifier'][0]['old_notation'] - cr.save() - except CatalogRecord.DoesNotExist: - pass - -class Migration(migrations.Migration): - - dependencies = [ - ('metax_api', '0026_auto_20200917_1615'), - ] - - operations = [ - migrations.RunPython(rename, revert), - ] diff --git a/src/metax_api/migrations/datasets/SYKE_csv.csv b/src/metax_api/migrations/datasets/SYKE_csv.csv deleted file mode 100644 index 654a3edf..00000000 --- a/src/metax_api/migrations/datasets/SYKE_csv.csv +++ /dev/null @@ -1,240 +0,0 @@ -title;vanha url;access;vanha guid;uusi guid;Omistaja;uusi url;; -Asuinalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E4DBC005-823B-469F-9AAB-51450438F41A};Avoin;{E4DBC005-823B-469F-9AAB-51450438F41A};{0B77FA8C-7C42-4783-AA5C-868152E32BE0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0B77FA8C-7C42-4783-AA5C-868152E32BE0};; -Chlorophyll-a daily product for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C6DCCB67-99B6-449A-A36F-26A847E6D856};Avoin;{C6DCCB67-99B6-449A-A36F-26A847E6D856};{10962669-987F-4FFA-9D73-A96E61AD6992};Finnish Environment Intitute (SYKE);https://metadata.ymparisto.fi/dataset/{10962669-987F-4FFA-9D73-A96E61AD6992};; -Chlorophyll-a daily product for the Baltic Sea 2012-2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={681B6DFF-2B4C-4881-AF69-37AD15A2B16A};Avoin;{681B6DFF-2B4C-4881-AF69-37AD15A2B16A};;;https://metadata.ymparisto.fi/dataset/;; -Chlorophyll-a weekly composite for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={422E4BF9-D15B-4779-8ABD-3610BD645C5C};Avoin;{422E4BF9-D15B-4779-8ABD-3610BD645C5C};;;https://metadata.ymparisto.fi/dataset/;; -Corine maanpeite 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A8936D40-2159-468D-84CD-0A978609B968};Avoin;{A8936D40-2159-468D-84CD-0A978609B968};{A70BC25F-43A2-4C33-A42D-9C22D9D4A3DB};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A70BC25F-43A2-4C33-A42D-9C22D9D4A3DB};; -Corine maanpeite 2006;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4438F7E1-2927-4854-B8F8-0EE8E8822C53};Avoin;{4438F7E1-2927-4854-B8F8-0EE8E8822C53};{703D5B2B-4CCC-49A7-B9E9-92B2E1290CD3};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{703D5B2B-4CCC-49A7-B9E9-92B2E1290CD3};; -Corine maanpeite 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D54C552F-F7F7-489B-8B1E-E093D93C7386};Avoin;{D54C552F-F7F7-489B-8B1E-E093D93C7386};{66D9A881-EE3C-42AD-9416-014EA6B84D23};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{66D9A881-EE3C-42AD-9416-014EA6B84D23};; -Harva ja tiheä taajama-alue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={674D6DB4-5FD9-434A-855F-F51316704630};Avoin;{674D6DB4-5FD9-434A-855F-F51316704630};{394B169F-2AE6-4966-8055-C593488F8898};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{394B169F-2AE6-4966-8055-C593488F8898};; -Havaitut tulva-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={62C30F1D-570D-4C75-80AE-6FE5402ECBBE};Avoin;{62C30F1D-570D-4C75-80AE-6FE5402ECBBE};{D8DE03E4-3F49-4BC1-9C58-3FADFB0F369A};;https://metadata.ymparisto.fi/dataset/{D8DE03E4-3F49-4BC1-9C58-3FADFB0F369A};; -Hydrologiset havaintopaikat, haihdunta-asemat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={72C2B39E-E581-4B5B-8C28-E5E0190DD97F};Avoin;{72C2B39E-E581-4B5B-8C28-E5E0190DD97F};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, jäänpaksuus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AB16A1FF-65E1-4A6E-A73D-653DDAAA7495};Avoin;{AB16A1FF-65E1-4A6E-A73D-653DDAAA7495};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, lumilinjamittaus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3D7A5268-E65C-4AB1-9A5E-99C8909AD764};Avoin;{3D7A5268-E65C-4AB1-9A5E-99C8909AD764};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, lämpötilaluotaus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={854C80BE-3383-4E67-8C21-0E4D038B7BA8};Avoin;{854C80BE-3383-4E67-8C21-0E4D038B7BA8};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, pintaveden lämpötila;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8462E50B-16BD-4C7F-9A79-F3DC6BFA1080};Avoin;{8462E50B-16BD-4C7F-9A79-F3DC6BFA1080};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, valunta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E11A44F-4987-4FF2-B55B-A3F94739FEE5};Avoin;{1E11A44F-4987-4FF2-B55B-A3F94739FEE5};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, vedenkorkeus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FC8A217-6A9D-4FE2-8923-843DA4A7DBE5};Avoin;{6FC8A217-6A9D-4FE2-8923-843DA4A7DBE5};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, virtaama;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B6AFC4CE-7827-46BE-9372-141CE59394AA};Avoin;{B6AFC4CE-7827-46BE-9372-141CE59394AA};;;https://metadata.ymparisto.fi/dataset/;; -INSPIRE-tietotuote: Luonnonsuojelu- ja erämaa-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DFC5480E-CFC4-4FF3-B42F-9685481309BC};Avoin;{DFC5480E-CFC4-4FF3-B42F-9685481309BC};;;https://metadata.ymparisto.fi/dataset/;; -INSPIRE-tietotuote: Natura 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EB7FEE73-535C-49D8-AD0B-EAF13E52E665};Avoin;{EB7FEE73-535C-49D8-AD0B-EAF13E52E665};;;https://metadata.ymparisto.fi/dataset/;; -INSPIRE-tietotuote: Uomaverkosto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6AF05F3B-0D56-49D5-BFEA-F1652620D879};Avoin;{6AF05F3B-0D56-49D5-BFEA-F1652620D879};;;https://metadata.ymparisto.fi/dataset/;; -Itämeren päivittäiset pintalämpötilat / Sea surface temperature for the Baltic Sea;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4568E834-419F-4295-8CB1-2BE76C1F3318};Avoin;{4568E834-419F-4295-8CB1-2BE76C1F3318};;;https://metadata.ymparisto.fi/dataset/;; -Itämeren viikottaiset pintalämpötilat / Sea surface temperature weekly composites for the Baltic Sea;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1B235B6E-170C-4EF4-BE20-77C606F1A165};Avoin;{1B235B6E-170C-4EF4-BE20-77C606F1A165};;;https://metadata.ymparisto.fi/dataset/;; -Järvien ja jokien luotauspisteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={28041FC2-F80F-43FF-BEAA-CE66D449D04F};Avoin;{28041FC2-F80F-43FF-BEAA-CE66D449D04F};{80B4FB0F-C60E-4290-8183-1309E8AD8B76};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{80B4FB0F-C60E-4290-8183-1309E8AD8B76};; -Järvien ja jokien syvyysaineisto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E3647EED-2697-4EDE-92C9-900DD458B87F};Avoin;{E3647EED-2697-4EDE-92C9-900DD458B87F};{DC4AD1E9-F4E5-4998-825D-D2CA4836A2F5};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{DC4AD1E9-F4E5-4998-825D-D2CA4836A2F5};; -Kaupunki-maaseutu-luokitus (YKR);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BDD349C3-3656-4B3C-A590-77901C058DA2};Avoin;{BDD349C3-3656-4B3C-A590-77901C058DA2};{A2556B8B-0E17-4E70-AF87-0FCEBDADBE89};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A2556B8B-0E17-4E70-AF87-0FCEBDADBE89};; -Keskustat ja kaupan alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7D2F780B-39D7-4C83-9A2A-C3E3D6534B61};Avoin;{7D2F780B-39D7-4C83-9A2A-C3E3D6534B61};{79B6DD92-3448-402D-95A9-D56B484CD6A1};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{79B6DD92-3448-402D-95A9-D56B484CD6A1};; -Koskiensuojelulailla suojellut vesistöt;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={48CD102E-D5CE-445A-B9DC-74F8E9345D94};Avoin;{48CD102E-D5CE-445A-B9DC-74F8E9345D94};{CF5DEA48-E8E4-49AA-8EDB-DBE862B92F07};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CF5DEA48-E8E4-49AA-8EDB-DBE862B92F07};; -Lajit, luontodirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4E1FF620-E911-4678-83A4-F4A13944BD1A};Avoin;{4E1FF620-E911-4678-83A4-F4A13944BD1A};{4F51E747-E3DC-4223-BD0D-50108396F8A1};;https://metadata.ymparisto.fi/dataset/{4F51E747-E3DC-4223-BD0D-50108396F8A1};; -Lake Chl-a products from Finland (MERIS, FRESHMON);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=CHL_Finland_SYKE_2006-2011_MERIS_FUB_RECT;Avoin;CHL_Finland_SYKE_2006-2011_MERIS_FUB_RECT;;;https://metadata.ymparisto.fi/dataset/;; -Liikennealueiden ympäristöriskiluokitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F41D131D-B103-4726-B291-E43B49D136A2};Avoin;{F41D131D-B103-4726-B291-E43B49D136A2};{0143CB1F-4203-4252-B042-E32CB570A930};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0143CB1F-4203-4252-B042-E32CB570A930};; -Linnut, lintudirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={084531FC-EE76-418A-9832-5F8B418BA60E};Avoin;{084531FC-EE76-418A-9832-5F8B418BA60E};{EEE47DAE-72EF-403A-AED4-7F5E78E53739};;https://metadata.ymparisto.fi/dataset/{EEE47DAE-72EF-403A-AED4-7F5E78E53739};; -Luonnonsuojelu- ja erämaa-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2627E9FE-B657-48E1-A98D-000D4CD5CA38};Avoin;{2627E9FE-B657-48E1-A98D-000D4CD5CA38};{C8FC4A42-A2C3-40C4-92CD-2299C688514E};Metsähallitus;https://metadata.ymparisto.fi/dataset/{C8FC4A42-A2C3-40C4-92CD-2299C688514E};; -Luonnonsuojeluohjelma-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C305FA65-F319-4FA0-AAB8-F92AE32B6EE2};Avoin;{C305FA65-F319-4FA0-AAB8-F92AE32B6EE2};{5A93FF5B-FB88-40C7-84E0-91FCC492A621};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5A93FF5B-FB88-40C7-84E0-91FCC492A621};; -Luontotyypit, luontodirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B67BC862-E17C-42A2-B7AE-70B8A171170F};Avoin;{B67BC862-E17C-42A2-B7AE-70B8A171170F};{A0DF9C40-CC7B-4359-8595-5C76B760257C};;https://metadata.ymparisto.fi/dataset/{A0DF9C40-CC7B-4359-8595-5C76B760257C};; -Maasto- ja vesiliikenteen rajoitusalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={01A88DB7-7672-4671-95C4-3CD2AEAC5A99};Avoin;{01A88DB7-7672-4671-95C4-3CD2AEAC5A99};{AFBC7507-2167-4224-8CBA-37E1B6C975A3};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AFBC7507-2167-4224-8CBA-37E1B6C975A3};; -Maisemamaakuntajako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8D4119C7-23C6-419C-B874-2FB4C8DAEC44};Avoin;{8D4119C7-23C6-419C-B874-2FB4C8DAEC44};{27ACED25-8784-484D-962B-3207459D8BF0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{27ACED25-8784-484D-962B-3207459D8BF0};; -Metsäkasvillisuusvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={56486ECE-92E9-4FA5-96F3-0FC1D1B8C428};Avoin;{56486ECE-92E9-4FA5-96F3-0FC1D1B8C428};{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};; -Metsäkasvillisuusvyöhykkeet uhanalaisarviointia varten;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FBF9037A-3381-47C4-AD09-BBEAB007FEC0};Avoin;{FBF9037A-3381-47C4-AD09-BBEAB007FEC0};{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};;https://metadata.ymparisto.fi/dataset/{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};; -Natura2000 alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={36FA93DC-F156-427E-973E-E83F7AFC6306};Avoin;{36FA93DC-F156-427E-973E-E83F7AFC6306};{ED80465E-135B-4391-AA8A-FE2038FB224D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{ED80465E-135B-4391-AA8A-FE2038FB224D};; -Pohjavesialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F8321822-0A0D-4AC4-81E3-B9E3BE6386A2};Avoin;{F8321822-0A0D-4AC4-81E3-B9E3BE6386A2};{8F45F7BF-669F-4434-A8DB-8E686933F6FF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8F45F7BF-669F-4434-A8DB-8E686933F6FF};; -Rakennussuojelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1500436E-FD0E-4560-A933-847F16EFCC94};Avoin;{1500436E-FD0E-4560-A933-847F16EFCC94};{46AC1915-EC9D-4A02-92C0-58DAD8301E4A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{46AC1915-EC9D-4A02-92C0-58DAD8301E4A};; -Ranta10 - rantaviiva 1:10 000 ja uomaverkosto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A40A94CB-4905-4489-9C03-52B6CE9F66CD};Avoin;{A40A94CB-4905-4489-9C03-52B6CE9F66CD};{E15CD0B3-3934-4ABC-BF23-A8C622FA6A57};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E15CD0B3-3934-4ABC-BF23-A8C622FA6A57};; -Residential Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={593D9E64-4289-4B7B-A946-7A59C763C904};Avoin;{593D9E64-4289-4B7B-A946-7A59C763C904};;;https://metadata.ymparisto.fi/dataset/;; -Satelliittikuvamosaiikki (Image2000);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AF0A925D-9410-4431-8DB8-0455C73D193E};Avoin;{AF0A925D-9410-4431-8DB8-0455C73D193E};{0FD2F424-2C22-4384-A0C6-65AF19DED2A9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0FD2F424-2C22-4384-A0C6-65AF19DED2A9};; -Satelliittikuvamosaiikki (Image2006);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F1B27979-9B92-4607-B97F-28A5808AD5CC};Avoin;{F1B27979-9B92-4607-B97F-28A5808AD5CC};{5F95B099-0EAC-44DC-A1D5-B5B4A388A862};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5F95B099-0EAC-44DC-A1D5-B5B4A388A862};; -Satelliittikuvamosaiikki (Image2012);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={85FE6E30-90D2-4DB9-901B-7D86A74599DD};Avoin;{85FE6E30-90D2-4DB9-901B-7D86A74599DD};{94D48E72-5150-4D21-86E6-361506DAD28A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{94D48E72-5150-4D21-86E6-361506DAD28A};; -Siltojen korkeudet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7E8EA280-EAD4-4AFF-BA43-73DCA7805836};Avoin;{7E8EA280-EAD4-4AFF-BA43-73DCA7805836};{07943EAC-6AF9-4EB1-9B31-B47199446476};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{07943EAC-6AF9-4EB1-9B31-B47199446476};; -Soidensuojelun täydennysehdotus, Etelä-Suomi ja valtionmaan toteutuneet kohteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={5F14DCDF-6A2C-4A67-9035-BC73D0ED60B0};Avoin;{5F14DCDF-6A2C-4A67-9035-BC73D0ED60B0};{C0D331FF-68D8-4BD4-AEB3-B631AF824745};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C0D331FF-68D8-4BD4-AEB3-B631AF824745};; -Suokasvillisuusvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B6BD1CAD-BAB5-4473-AFD0-79DB30A88420};Avoin;{B6BD1CAD-BAB5-4473-AFD0-79DB30A88420};{193219CE-197F-4717-8D40-E7EDF6884A1E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{193219CE-197F-4717-8D40-E7EDF6884A1E};; -Tulvariskialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={83B57B72-1D1D-4DE4-BC61-180F3EE028AF};Avoin;{83B57B72-1D1D-4DE4-BC61-180F3EE028AF};{AF913A8D-484D-4313-ABD4-AE65A46A391E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AF913A8D-484D-4313-ABD4-AE65A46A391E};; -Tulvavaaravyöhykkeet, meritulva;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9EC02206-E14C-4186-AF09-829FFFB43FD9};Avoin;{9EC02206-E14C-4186-AF09-829FFFB43FD9};;;https://metadata.ymparisto.fi/dataset/;; -Tulvavaaravyöhykkeet, vesistötulva (avovesi);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C2B3FC73-060B-4053-BA04-54D86F4DE7E5};Avoin;{C2B3FC73-060B-4053-BA04-54D86F4DE7E5};;;https://metadata.ymparisto.fi/dataset/;; -Tunturialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A9B5E4AF-21B3-4FA5-AF45-2CD819CE742E};Avoin;{A9B5E4AF-21B3-4FA5-AF45-2CD819CE742E};{ADEAFCBE-9E2D-403A-8E78-13FE913D5E42};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{ADEAFCBE-9E2D-403A-8E78-13FE913D5E42};; -Uimavesidirektiivin mukaiset uimavedet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={918099FC-918C-4FA3-9444-5A3D5E950877};Avoin;{918099FC-918C-4FA3-9444-5A3D5E950877};{11F3E166-1B35-4628-BE18-45D1A09A097F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{11F3E166-1B35-4628-BE18-45D1A09A097F};; -Valtakunnallisesti arvokkaat kallioalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4CFB1D5B-F47E-4E92-918A-63EC5BB9C217};Avoin;{4CFB1D5B-F47E-4E92-918A-63EC5BB9C217};{626BAE8B-6632-43FD-8B3A-CF1E01662331};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{626BAE8B-6632-43FD-8B3A-CF1E01662331};; -Valtakunnallisesti arvokkaat moreenimuodostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A21026BF-204C-42FC-9FCB-450BD5A94C81};Avoin;{A21026BF-204C-42FC-9FCB-450BD5A94C81};{8253487E-FA94-4E83-9A85-71F85D4DA0B0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8253487E-FA94-4E83-9A85-71F85D4DA0B0};; -Valtakunnallisesti arvokkaat tuuli- ja rantakerrostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={66A5653E-A8E7-4BA3-9703-A0AF001AE089};Avoin;{66A5653E-A8E7-4BA3-9703-A0AF001AE089};{3298587D-C391-47C6-A4E0-D599D0206B73};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3298587D-C391-47C6-A4E0-D599D0206B73};; -Valuma-aluejako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={343C40A6-D0ED-4B6A-BA77-2724A48A3BD3};Avoin;{343C40A6-D0ED-4B6A-BA77-2724A48A3BD3};{44394B13-85D7-4998-BD06-8ADC77C7455C};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{44394B13-85D7-4998-BD06-8ADC77C7455C};; -Water turbidity daily product for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={89F4653E-9930-4540-BFBA-C78444E1FCD5};Avoin;{89F4653E-9930-4540-BFBA-C78444E1FCD5};{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};Finnish Environment Intitute (SYKE);https://metadata.ymparisto.fi/dataset/{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};; -Water turbidity daily product for the Baltic Sea 2012-2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B4783610-3298-4550-9793-9DBDCB0080B1};Avoin;{B4783610-3298-4550-9793-9DBDCB0080B1};;;https://metadata.ymparisto.fi/dataset/;; -Water turbidity weekly composite for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CEF2EA78-0400-46F1-8173-CD1D1EE547D1};Avoin;{CEF2EA78-0400-46F1-8173-CD1D1EE547D1};{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};;https://metadata.ymparisto.fi/dataset/{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};; -Vesienhoitoalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DFA8CF50-721B-4C71-93DD-A8264C5C2A9C};Avoin;{DFA8CF50-721B-4C71-93DD-A8264C5C2A9C};{6AF42DE4-EB9A-4568-9E6C-90BEF19673D0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6AF42DE4-EB9A-4568-9E6C-90BEF19673D0};; -Vesipuitedirektiivin mukaiset vesimuodostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CB8FEF32-5FCE-4E33-A8B8-1FBB02F9C2C1};Avoin;{CB8FEF32-5FCE-4E33-A8B8-1FBB02F9C2C1};{A3748B80-6E63-4460-A8B7-72CB9884F69B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A3748B80-6E63-4460-A8B7-72CB9884F69B};; -Yhdyskuntarakenteen vyöhykkeet 2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={96F338EA-75AF-432C-A780-31A3CDECBDF2};Avoin;{96F338EA-75AF-432C-A780-31A3CDECBDF2};{3B8BD61A-C137-46ED-B16A-B8BA27D1561B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3B8BD61A-C137-46ED-B16A-B8BA27D1561B};; -YKR Kaupunkiseudut;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={29205135-177D-4244-ABF3-BEE74782E567};Avoin;{29205135-177D-4244-ABF3-BEE74782E567};{E3E29357-4333-4C11-8E0E-FCD7307F91EE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E3E29357-4333-4C11-8E0E-FCD7307F91EE};; -YKR-aluejaot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={802F4259-8AB3-473A-810C-FA3BE4FE5C92};Avoin;{802F4259-8AB3-473A-810C-FA3BE4FE5C92};{A2CEE8A2-F188-4787-88FC-A393032CA60A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A2CEE8A2-F188-4787-88FC-A393032CA60A};; -YKR-taajama;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CB02E608-6E6B-417E-87DA-BD7C8CB53712};Avoin;{CB02E608-6E6B-417E-87DA-BD7C8CB53712};{28F79998-B5E2-4085-AF80-FB3FA5E9F9BA};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{28F79998-B5E2-4085-AF80-FB3FA5E9F9BA};; -Ympäristömeludirektiivin mukaiset melualueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9D872FA-3A6D-45C6-AD52-78E0724307A7};Avoin;{E9D872FA-3A6D-45C6-AD52-78E0724307A7};;;https://metadata.ymparisto.fi/dataset/;; -Ympäristömeludirektiivin mukaiset melualueet 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8B962E44-AB11-4D38-B178-76AB85B114BB};Avoin;{8B962E44-AB11-4D38-B178-76AB85B114BB};{FCF0230E-07C1-48FF-A762-B51F1332F1DC};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{FCF0230E-07C1-48FF-A762-B51F1332F1DC};; -Aluesadannan ja lumen aluevesiarvon laskenta-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9CC3C6F8-D834-4663-8424-D846CEE43F3D};Saatavuutta rajoitettu;{9CC3C6F8-D834-4663-8424-D846CEE43F3D};{248E7E3F-02A6-41AF-A977-5A4949966001};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{248E7E3F-02A6-41AF-A977-5A4949966001};; -ARA-rakennukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E97AFFB-BFDB-4077-BEDD-BE5A7D971C8E};Saatavuutta rajoitettu;{1E97AFFB-BFDB-4077-BEDD-BE5A7D971C8E};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Areas Included in National Conservation Programmes;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A0C487D4-968B-4553-9EFB-870D6D2A728C};Saatavuutta rajoitettu;{A0C487D4-968B-4553-9EFB-870D6D2A728C};;;https://metadata.ymparisto.fi/dataset/;; -Areas with Local Detailed Plans;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E60A3152-F0AA-4E61-BA1C-E28F0DBE8FF6};Saatavuutta rajoitettu;{E60A3152-F0AA-4E61-BA1C-E28F0DBE8FF6};;;https://metadata.ymparisto.fi/dataset/;; -Asemakaavoitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AC7E17C3-0833-40B1-B37A-187320A03826};Saatavuutta rajoitettu;{AC7E17C3-0833-40B1-B37A-187320A03826};{31968F40-0083-413A-B474-A6D2DB081EDF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{31968F40-0083-413A-B474-A6D2DB081EDF};; -Asemakaavojen seurantalomakkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9BCE41E0-54BC-43A4-9543-D3948FD151EF};Saatavuutta rajoitettu;{9BCE41E0-54BC-43A4-9543-D3948FD151EF};{9BCE41E0-54BC-43A4-9543-D3948FD151EF};Suomen ympäristökeskus / Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{9BCE41E0-54BC-43A4-9543-D3948FD151EF};; -Asianhallintajärjestelmä AHJO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DBBDEECB-AB2A-4B01-B45B-DE809992F79F};Saatavuutta rajoitettu;{DBBDEECB-AB2A-4B01-B45B-DE809992F79F};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Asuinrakennusten tiheys (rakennusta/km2);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={44F16A49-3DC8-4BB5-9420-6D015FB64EF1};Saatavuutta rajoitettu;{44F16A49-3DC8-4BB5-9420-6D015FB64EF1};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -AVOIMUUSINDEKSI MERENPOHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0189B2FD-4411-4CB0-90E5-DF4E1293C6F3};Saatavuutta rajoitettu;{0189B2FD-4411-4CB0-90E5-DF4E1293C6F3};{EB99F934-DCF8-4383-BD38-73B2DF3F0C72};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{EB99F934-DCF8-4383-BD38-73B2DF3F0C72};; -Bathing Waters;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1D154349-AFCA-4FBB-AD07-50EB6F36339D};Saatavuutta rajoitettu;{1D154349-AFCA-4FBB-AD07-50EB6F36339D};;;https://metadata.ymparisto.fi/dataset/;; -BCU-konttipaikat (öljyyntyneiden lintujen hoitoon);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EFC9B874-90BF-4701-BABF-29F6F7D58BE0};Saatavuutta rajoitettu;{EFC9B874-90BF-4701-BABF-29F6F7D58BE0};{DF0A5826-F1DF-42EB-A7A8-B8510A46B675};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{DF0A5826-F1DF-42EB-A7A8-B8510A46B675};; -BORIS 2 (Baltic Oil Response Information System);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={180ACCFE-FE84-4CD3-AF8A-6FF75C473059};Saatavuutta rajoitettu;{180ACCFE-FE84-4CD3-AF8A-6FF75C473059};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Catchment areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D6C6858A-562D-4965-AD77-2B1E97EFFA6B};Saatavuutta rajoitettu;{D6C6858A-562D-4965-AD77-2B1E97EFFA6B};;;https://metadata.ymparisto.fi/dataset/;; -CERAMIUM TENUICORNE LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D30F72F9-01F0-448A-9F18-213954BF87C7};Saatavuutta rajoitettu;{D30F72F9-01F0-448A-9F18-213954BF87C7};{F274E613-65AB-4867-B2C6-925596B7A8DE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F274E613-65AB-4867-B2C6-925596B7A8DE};; -CHARA ASPERA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F271AB26-3268-461F-A380-F3A1CC6AB62E};Saatavuutta rajoitettu;{F271AB26-3268-461F-A380-F3A1CC6AB62E};{BF849316-273C-4347-B151-D4F06460C314};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF849316-273C-4347-B151-D4F06460C314};; -CHARA SPP. & NITELLA SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={31CF9F00-FAA6-4158-B2E9-9D443776C4B1};Saatavuutta rajoitettu;{31CF9F00-FAA6-4158-B2E9-9D443776C4B1};;;https://metadata.ymparisto.fi/dataset/;; -CHARA TOMENTOSA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3895111B-8E40-4EC1-9256-B4C59B460832};Saatavuutta rajoitettu;{3895111B-8E40-4EC1-9256-B4C59B460832};{0408513A-8CCF-422F-A47B-F12F0946D6EA};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0408513A-8CCF-422F-A47B-F12F0946D6EA};; -CHORDA FILUM LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7D430B8-AEA0-4701-BD8F-533892EA0AD4};Saatavuutta rajoitettu;{B7D430B8-AEA0-4701-BD8F-533892EA0AD4};{CE550B14-5568-4C84-A67C-1473FC33E71F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CE550B14-5568-4C84-A67C-1473FC33E71F};; -CLADOPHORA GLOMERATA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={609B6F9F-954E-4F98-BCFC-172689200290};Saatavuutta rajoitettu;{609B6F9F-954E-4F98-BCFC-172689200290};{411FD599-AAC9-491D-95CA-BC942AE2FF72};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{411FD599-AAC9-491D-95CA-BC942AE2FF72};; -CLADOPHORA RUPESTRIS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0B5AAC11-E3E2-40D3-800F-8F5801F503AB};Saatavuutta rajoitettu;{0B5AAC11-E3E2-40D3-800F-8F5801F503AB};{37D789D3-A7E2-44FA-B227-58B59F962244};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{37D789D3-A7E2-44FA-B227-58B59F962244};; -Corine Land Cover 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EEC35D81-765A-4A7B-BC9B-F89A476807EC};Saatavuutta rajoitettu;{EEC35D81-765A-4A7B-BC9B-F89A476807EC};;;https://metadata.ymparisto.fi/dataset/;; -Corine Land Cover 2006;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DD648E4F-0478-4D32-8286-C2437EC87D33};Saatavuutta rajoitettu;{DD648E4F-0478-4D32-8286-C2437EC87D33};;;https://metadata.ymparisto.fi/dataset/;; -Corine Land Cover 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A58329D8-3ED0-47B8-B789-3095EE55C255};Saatavuutta rajoitettu;{A58329D8-3ED0-47B8-B789-3095EE55C255};;;https://metadata.ymparisto.fi/dataset/;; -Elinympäristön tietopalvelu Liiteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={629AC114-8BBC-48B5-9D8F-303A53CF3E9F};Saatavuutta rajoitettu;{629AC114-8BBC-48B5-9D8F-303A53CF3E9F};{1893C260-D934-48F8-A0A0-3606FC392438};Suomen ympäristökeskus / Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{1893C260-D934-48F8-A0A0-3606FC392438};; -Eliölajit-tietojärjestelmä TAXON;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C420898F-600A-41CB-8843-F06A90301427};Saatavuutta rajoitettu;{C420898F-600A-41CB-8843-F06A90301427};{C420898F-600A-41CB-8843-F06A90301427};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C420898F-600A-41CB-8843-F06A90301427};; -Energiantuotanto rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9707160B-8B54-48DB-9151-4A41FD46D159};Saatavuutta rajoitettu;{9707160B-8B54-48DB-9151-4A41FD46D159};{DBEAADBD-3EE0-498D-8EC2-C66191DA559C};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{DBEAADBD-3EE0-498D-8EC2-C66191DA559C};; -Environmental Noise Maps;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0CF94C00-D59A-45A0-A2EA-4ACF9014A2BA};Saatavuutta rajoitettu;{0CF94C00-D59A-45A0-A2EA-4ACF9014A2BA};;;https://metadata.ymparisto.fi/dataset/;; -EPHYDATIA FLUVIATILIS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB5AE7E2-8C84-4FCC-83DD-1150F60EEE27};Saatavuutta rajoitettu;{BB5AE7E2-8C84-4FCC-83DD-1150F60EEE27};{20637AA7-97D7-43AB-AF34-636043F1C1B9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{20637AA7-97D7-43AB-AF34-636043F1C1B9};; -Etäisyysvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={64764F78-0F35-438B-9EA4-FDAC3384840D};Saatavuutta rajoitettu;{64764F78-0F35-438B-9EA4-FDAC3384840D};{BF51D501-58F4-4932-A951-5B5A88F4D3C6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF51D501-58F4-4932-A951-5B5A88F4D3C6};; -Flood hazard areas, lake and river floods;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={302A9608-99B9-4D10-A469-36949D12436F};Saatavuutta rajoitettu;{302A9608-99B9-4D10-A469-36949D12436F};;;https://metadata.ymparisto.fi/dataset/;; -Flood hazard areas, sea floods;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6C21A825-FE70-47CE-91CA-E14EBE01F562};Saatavuutta rajoitettu;{6C21A825-FE70-47CE-91CA-E14EBE01F562};;;https://metadata.ymparisto.fi/dataset/;; -Flood Risk Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={13972098-7333-4827-B5D0-CFEF3CE54295};Saatavuutta rajoitettu;{13972098-7333-4827-B5D0-CFEF3CE54295};;;https://metadata.ymparisto.fi/dataset/;; -FONTINALIS SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={83CE79F8-4DF5-4B8A-B431-77FAC6195603};Saatavuutta rajoitettu;{83CE79F8-4DF5-4B8A-B431-77FAC6195603};{6EF6BCA9-D30B-45C1-8111-641DC55FA095};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6EF6BCA9-D30B-45C1-8111-641DC55FA095};; -Forest Vegetation Zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D53E9674-A446-4F54-81C3-8C15310353CC};Saatavuutta rajoitettu;{D53E9674-A446-4F54-81C3-8C15310353CC};;;https://metadata.ymparisto.fi/dataset/;; -FUCUS SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FB02515B-D7A9-4F6D-BD15-2E2B818AD997};Saatavuutta rajoitettu;{FB02515B-D7A9-4F6D-BD15-2E2B818AD997};{B6B48CE3-B9FA-442A-9FF5-F8E0B396578D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{B6B48CE3-B9FA-442A-9FF5-F8E0B396578D};; -FUCUS-YHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={76879C3D-8293-4C12-B873-26F59BCF54B9};Saatavuutta rajoitettu;{76879C3D-8293-4C12-B873-26F59BCF54B9};;;https://metadata.ymparisto.fi/dataset/;; -GOFREP-alue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B9487289-0C02-48E4-A26F-DAACDA60C462};Saatavuutta rajoitettu;{B9487289-0C02-48E4-A26F-DAACDA60C462};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Groundwater areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FA1742D1-8509-437C-846A-6637B3FF7345};Saatavuutta rajoitettu;{FA1742D1-8509-437C-846A-6637B3FF7345};;;https://metadata.ymparisto.fi/dataset/;; -HAURA- JA HAPSIKKAPOHJAT LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FF12F76-3162-4DD9-8320-668EDB9CBD9E};Saatavuutta rajoitettu;{6FF12F76-3162-4DD9-8320-668EDB9CBD9E};{C422872A-BA11-406C-9263-44D3302A7FCD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C422872A-BA11-406C-9263-44D3302A7FCD};; -Helsingin ihmispaineindeksi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={43163BBB-0D01-4375-BE28-33AA1607B2C3};Saatavuutta rajoitettu;{43163BBB-0D01-4375-BE28-33AA1607B2C3};{2063FB74-4879-4213-B9B5-E33FC848315E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2063FB74-4879-4213-B9B5-E33FC848315E};; -Historic Flood Maps;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FF5E8D4-5329-44D1-8741-D7F3223D0F30};Saatavuutta rajoitettu;{6FF5E8D4-5329-44D1-8741-D7F3223D0F30};;;https://metadata.ymparisto.fi/dataset/;; -HUB-biotooppiluokittelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7DB140D8-F133-42DB-B424-3309C2684A6D};Saatavuutta rajoitettu;{7DB140D8-F133-42DB-B424-3309C2684A6D};{E0001193-53A2-4CA3-B849-753621E51A69};Metsähallitus / Matti Sahla;https://metadata.ymparisto.fi/dataset/{E0001193-53A2-4CA3-B849-753621E51A69};; -Hydrologian ja vesien käytön tietojärjestelmä HYDRO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={86FC3188-6796-4C79-AC58-8DBC7B568827};Saatavuutta rajoitettu;{86FC3188-6796-4C79-AC58-8DBC7B568827};{86FC3188-6796-4C79-AC58-8DBC7B568827};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{86FC3188-6796-4C79-AC58-8DBC7B568827};; -Hätäkeskukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={51C9D60D-6D41-44BD-9136-C4933510DB2D};Saatavuutta rajoitettu;{51C9D60D-6D41-44BD-9136-C4933510DB2D};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Ilmapäästötietojärjestelmä IPTJ;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={033E574E-3B6D-4A40-A2BC-8C50C8FA86E0};Saatavuutta rajoitettu;{033E574E-3B6D-4A40-A2BC-8C50C8FA86E0};;;https://metadata.ymparisto.fi/dataset/;; -Jokisuistot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6DEF8642-7867-4047-AEB9-5134D24C4D64};Saatavuutta rajoitettu;{6DEF8642-7867-4047-AEB9-5134D24C4D64};{5B7F87B2-F082-4081-B653-3AEF12446C9E};Metsähallitus;https://metadata.ymparisto.fi/dataset/{5B7F87B2-F082-4081-B653-3AEF12446C9E};; -Järvirekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E4505311-A27A-4CD5-B207-89EFEEFE48CF};Saatavuutta rajoitettu;{E4505311-A27A-4CD5-B207-89EFEEFE48CF};{E4505311-A27A-4CD5-B207-89EFEEFE48CF};Suomen ympäristökeskus /Vesikeskus;https://metadata.ymparisto.fi/dataset/{E4505311-A27A-4CD5-B207-89EFEEFE48CF};; -Järvirekisteri / Säännöstelyhankkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};Saatavuutta rajoitettu;{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};; -Jätevedenpuhdistamot rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CE26C069-1F54-4F78-8FE2-A1C025E8C9E8};Saatavuutta rajoitettu;{CE26C069-1F54-4F78-8FE2-A1C025E8C9E8};{E1663280-9639-4559-B0EA-18D1B4BD5625};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{E1663280-9639-4559-B0EA-18D1B4BD5625};; -Kaavoituksen seurannan tilasto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4A29A502-D287-4956-AD84-8C467D1E4D40};Saatavuutta rajoitettu;{4A29A502-D287-4956-AD84-8C467D1E4D40};{4A29A502-D287-4956-AD84-8C467D1E4D40};Suomen ympäristökeskus/Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{4A29A502-D287-4956-AD84-8C467D1E4D40};; -Kalankasvatus ja verkkoaltaat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F4A6DADE-650B-4BA2-8E69-11ED37EC1335};Saatavuutta rajoitettu;{F4A6DADE-650B-4BA2-8E69-11ED37EC1335};{D2396FA4-DFF0-4D88-A647-70256C18941E};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{D2396FA4-DFF0-4D88-A647-70256C18941E};; -Kansainväliset jätesiirrot -rekisteri (Basel-rekisteri);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8D42F658-9DAD-4C65-9418-ABB00B92DF98};Saatavuutta rajoitettu;{8D42F658-9DAD-4C65-9418-ABB00B92DF98};{8D42F658-9DAD-4C65-9418-ABB00B92DF98};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8D42F658-9DAD-4C65-9418-ABB00B92DF98};; -Kansalliset kaupunkipuistot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={ECE5D7C1-FF3C-47C2-84F1-D878CD3DA6EE};Saatavuutta rajoitettu;{ECE5D7C1-FF3C-47C2-84F1-D878CD3DA6EE};{43E99E3C-7C91-42F0-A324-C9CB086E65DD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{43E99E3C-7C91-42F0-A324-C9CB086E65DD};; -Kasviplanktontietojärjestelmä KPLANK;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2F164F22-A4F9-468A-97E5-717B0B3D0E7B};Saatavuutta rajoitettu;{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};;https://metadata.ymparisto.fi/dataset/{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};; -Kemikaalien ympäristötietorekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={04B79E94-8D3B-451E-963B-19DDAD6F6215};Saatavuutta rajoitettu;{04B79E94-8D3B-451E-963B-19DDAD6F6215};;;https://metadata.ymparisto.fi/dataset/;; -Kertymärekisteri KERTY;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FF5FE8B1-B2BC-44C7-8A47-744F0A522CD2};Saatavuutta rajoitettu;{FF5FE8B1-B2BC-44C7-8A47-744F0A522CD2};;;https://metadata.ymparisto.fi/dataset/;; -Koekalastusrekisteri (Nordic-verkot, Coastal-verkot, sähkökoekalastus);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={348DFB97-5F9F-4902-9152-23B0C9505673};Saatavuutta rajoitettu;{348DFB97-5F9F-4902-9152-23B0C9505673};;Luonnonvarakeskus;https://metadata.ymparisto.fi/dataset/;; -Kuntien poikkeamispäätökset ja suunnittelutarveratkaisut sekä ELY-keskusten tekemät poikkeamispäätökset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0A81369B-6693-4BEB-80EF-238035480C3C};Saatavuutta rajoitettu;{0A81369B-6693-4BEB-80EF-238035480C3C};;;https://metadata.ymparisto.fi/dataset/;; -Laboratorion ja näytteenoton tiedonhallintajärjestelmä LIMS;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={939AE4E5-332A-4B0C-B9BF-96E0F16620A0};Saatavuutta rajoitettu;{939AE4E5-332A-4B0C-B9BF-96E0F16620A0};;;https://metadata.ymparisto.fi/dataset/;; -Lake and River Depth Profiles;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={23FB0660-5B2B-4212-BE02-772EB6E27E16};Saatavuutta rajoitettu;{23FB0660-5B2B-4212-BE02-772EB6E27E16};;;https://metadata.ymparisto.fi/dataset/;; -Laskeumarekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={19002DDF-91D0-48A3-8E68-891C8E7374D5};Saatavuutta rajoitettu;{19002DDF-91D0-48A3-8E68-891C8E7374D5};;;https://metadata.ymparisto.fi/dataset/;; -Liikennealueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F47A597F-FC14-47C5-BD3A-AE0710BC8A08};Saatavuutta rajoitettu;{F47A597F-FC14-47C5-BD3A-AE0710BC8A08};{22C3E0C5-3221-408E-B346-60493CCE6361};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{22C3E0C5-3221-408E-B346-60493CCE6361};; -Lumen peittämä ala Itämeren alueella 2007-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=Metadata_SYKE_SCA_Baltic_0.05deg_pv;Saatavuutta rajoitettu;Metadata_SYKE_SCA_Baltic_0.05deg_pv;;;https://metadata.ymparisto.fi/dataset/;; -Lumen peittämä ala Suomessa 2006-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=Metadata_SYKE_SCA_2006-2011_Finland;Saatavuutta rajoitettu;Metadata_SYKE_SCA_2006-2011_Finland;;;https://metadata.ymparisto.fi/dataset/;; -Maa-ainesluvat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={604DFF56-4157-46DE-84CB-71978DAC790A};Saatavuutta rajoitettu;{604DFF56-4157-46DE-84CB-71978DAC790A};{F163EC63-72A8-4FB5-A021-7A828BC1923F};KEHA-keskus;https://metadata.ymparisto.fi/dataset/{F163EC63-72A8-4FB5-A021-7A828BC1923F};; -Maaperän tilan tietojärjestelmä MATTI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};Saatavuutta rajoitettu;{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};Suomen ympäristökeskus ja ELY-keskukset;https://metadata.ymparisto.fi/dataset/{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};; -Maastoliikennerajoitukset-karttapalvelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E8222F77-00C4-4934-B2CB-0AB6A93D9E69};Saatavuutta rajoitettu;{E8222F77-00C4-4934-B2CB-0AB6A93D9E69};{8F7C8F06-B945-4D95-91BE-F0ECF1D76B7E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8F7C8F06-B945-4D95-91BE-F0ECF1D76B7E};; -Mahdolliset kapeat murtovesilahdet (1650);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8770CC62-F274-4700-9A81-592FBDE66635};Saatavuutta rajoitettu;{8770CC62-F274-4700-9A81-592FBDE66635};;;https://metadata.ymparisto.fi/dataset/;; -Mahdolliset laajat matalat lahdet (1160);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6655C0FA-9562-4015-B97A-0D07A19CF72C};Saatavuutta rajoitettu;{6655C0FA-9562-4015-B97A-0D07A19CF72C};;;https://metadata.ymparisto.fi/dataset/;; -Mahdolliset rannikon laguunit (1150);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={74345ACE-9D96-4A52-9E2F-7E8FE38C7809};Saatavuutta rajoitettu;{74345ACE-9D96-4A52-9E2F-7E8FE38C7809};;;https://metadata.ymparisto.fi/dataset/;; -MERENPOHJAN KIVIKKOISUUDEN TODENNÄKÖISYYS;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={41482CFB-D636-4396-AAB3-CBC82A28772A};Saatavuutta rajoitettu;{41482CFB-D636-4396-AAB3-CBC82A28772A};;;https://metadata.ymparisto.fi/dataset/;; -Merialuejako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3F43EA83-48C3-4D04-9DDF-67822FB32EF2};Saatavuutta rajoitettu;{3F43EA83-48C3-4D04-9DDF-67822FB32EF2};{6030C360-861F-4C16-B4BB-6A0EB4B5CF96};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6030C360-861F-4C16-B4BB-6A0EB4B5CF96};; -Merihiekan- ja soranottoalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={46D1A89C-7CE9-420E-B592-4018AC17EEE8};Saatavuutta rajoitettu;{46D1A89C-7CE9-420E-B592-4018AC17EEE8};{E35DE705-2986-43F2-8678-F219F1D097F2};Metsähallitus;https://metadata.ymparisto.fi/dataset/{E35DE705-2986-43F2-8678-F219F1D097F2};; -Meripelastusvastuualue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A2C05340-3276-4FBB-8350-607560E3C31F};Saatavuutta rajoitettu;{A2C05340-3276-4FBB-8350-607560E3C31F};{22A9AD4A-E725-4426-BD1A-916A23CF73BE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{22A9AD4A-E725-4426-BD1A-916A23CF73BE};; -MERIVEDEN LÄMPÖTILA POHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={ED3D3E91-8674-4532-8E78-3E63950A9BE5};Saatavuutta rajoitettu;{ED3D3E91-8674-4532-8E78-3E63950A9BE5};{8E562E41-5465-40A1-9990-4EACE2537F01};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8E562E41-5465-40A1-9990-4EACE2537F01};; -Meriveden suolaisuus pinnalla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EA4227EC-6958-4EC1-AE3C-3051FB9678B8};Saatavuutta rajoitettu;{EA4227EC-6958-4EC1-AE3C-3051FB9678B8};{D9F7BA00-14B9-40A0-9FEB-A9549266D7FF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D9F7BA00-14B9-40A0-9FEB-A9549266D7FF};; -MERIVEDEN SUOLAISUUS POHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7E5A9C8-08A2-4272-9AB2-ADA81A2E6C69};Saatavuutta rajoitettu;{B7E5A9C8-08A2-4272-9AB2-ADA81A2E6C69};;;https://metadata.ymparisto.fi/dataset/;; -Mire vegetation zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={79F1A09C-54C6-4EBE-96D8-1970459896EE};Saatavuutta rajoitettu;{79F1A09C-54C6-4EBE-96D8-1970459896EE};;;https://metadata.ymparisto.fi/dataset/;; -MRL:n mukaisten yleiskaavojen ulkorajaukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D8DD3305-0313-47F4-B241-71658B6C06AC};Saatavuutta rajoitettu;{D8DD3305-0313-47F4-B241-71658B6C06AC};;;https://metadata.ymparisto.fi/dataset/;; -MYTILUS TROSSULUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FECA3DEC-6C77-4424-BC42-0AE1452B2BA0};Saatavuutta rajoitettu;{FECA3DEC-6C77-4424-BC42-0AE1452B2BA0};{6AC94879-B22D-42DA-B3F5-3FC955AE03E4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6AC94879-B22D-42DA-B3F5-3FC955AE03E4};; -NAJAS MARINA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6906D7D5-BAEC-4006-8FDB-84732B61ADE7};Saatavuutta rajoitettu;{6906D7D5-BAEC-4006-8FDB-84732B61ADE7};{61E89CB2-03BA-4FD5-A231-BA2D909D30EF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{61E89CB2-03BA-4FD5-A231-BA2D909D30EF};; -Nannut-luokittelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8A4E6487-B27A-4F3B-8EA4-4651EA39ACCE};Saatavuutta rajoitettu;{8A4E6487-B27A-4F3B-8EA4-4651EA39ACCE};{F60E6BF7-9BC6-476D-8102-2E310A204BE0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F60E6BF7-9BC6-476D-8102-2E310A204BE0};; -National Database of Regional Land Use Plans;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2FAA5D6B-C053-465E-812C-119798581F5C};Saatavuutta rajoitettu;{2FAA5D6B-C053-465E-812C-119798581F5C};;;https://metadata.ymparisto.fi/dataset/;; -National Landscape Division;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={09F5613F-D9A2-4C68-818D-70B018437888};Saatavuutta rajoitettu;{09F5613F-D9A2-4C68-818D-70B018437888};;;https://metadata.ymparisto.fi/dataset/;; -Nationally Designated Nature Protected Areas and Wilderness Reserves;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9871C541-9E84-4241-A1E5-51C596A5A4E2};Saatavuutta rajoitettu;{9871C541-9E84-4241-A1E5-51C596A5A4E2};;;https://metadata.ymparisto.fi/dataset/;; -Nationally Valuable Aeolian and Littoral Deposits;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={04399CD4-86CD-453E-B683-5CFF53FA36D0};Saatavuutta rajoitettu;{04399CD4-86CD-453E-B683-5CFF53FA36D0};;;https://metadata.ymparisto.fi/dataset/;; -Nationally Valuable Moraine Formations;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0193799E-1F36-4FB9-A0B9-48264860C58C};Saatavuutta rajoitettu;{0193799E-1F36-4FB9-A0B9-48264860C58C};;;https://metadata.ymparisto.fi/dataset/;; -Nationally Valuable Rocky Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2753EE72-1F06-4291-987F-08FA693ED5CA};Saatavuutta rajoitettu;{2753EE72-1F06-4291-987F-08FA693ED5CA};;;https://metadata.ymparisto.fi/dataset/;; -Natura 2000 -tietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7AC30A3E-8E38-4717-8D2E-80585623212B};Saatavuutta rajoitettu;{7AC30A3E-8E38-4717-8D2E-80585623212B};;Suomen ympäristökeskus / Luontoympäristökeskus / Biodiversiteettiyksikkö;https://metadata.ymparisto.fi/dataset/;; -Nature Recreational Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9F0B1FD-C41E-46A1-BA30-E9A29DF12D70};Saatavuutta rajoitettu;{E9F0B1FD-C41E-46A1-BA30-E9A29DF12D70};;;https://metadata.ymparisto.fi/dataset/;; -NÄKÖSYVYYS (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={55AB842F-9CED-4E80-A7E5-07A54F0AE4A4};Saatavuutta rajoitettu;{55AB842F-9CED-4E80-A7E5-07A54F0AE4A4};{811B434E-75A1-4F53-9445-A4B4BDADA499};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{811B434E-75A1-4F53-9445-A4B4BDADA499};; -Olemassa oleva ja suunniteltu tuulivoima;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9B856E2E-6F8F-4B9A-8618-9923F238852D};Saatavuutta rajoitettu;{9B856E2E-6F8F-4B9A-8618-9923F238852D};{F690781C-D187-4611-8D80-A703804F7B59};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F690781C-D187-4611-8D80-A703804F7B59};; -Oppilaitokset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7753FF3C-D1E8-453A-B3F1-8412A2AD1CD1};Saatavuutta rajoitettu;{7753FF3C-D1E8-453A-B3F1-8412A2AD1CD1};;Tilastokeskus;https://metadata.ymparisto.fi/dataset/;; -Outlines of Local Master Plans Ratified in Accordance to the old Building Act;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6771A0AD-3B9C-450E-9536-0B036B47A662};Saatavuutta rajoitettu;{6771A0AD-3B9C-450E-9536-0B036B47A662};;;https://metadata.ymparisto.fi/dataset/;; -Outlines of Local Master Plans Ratified in Accordance with the Landuse and Building Act;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2D70E79B-36F8-4E5E-BB6C-0DD7C6E5639D};Saatavuutta rajoitettu;{2D70E79B-36F8-4E5E-BB6C-0DD7C6E5639D};;;https://metadata.ymparisto.fi/dataset/;; -Paikkatietoaineistojen latauspalvelu LAPIO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DDFEA0F7-7A42-4E8E-9782-57DD74697BD1};Saatavuutta rajoitettu;{DDFEA0F7-7A42-4E8E-9782-57DD74697BD1};{3EEF491C-53D4-4614-B64C-1D98A699198C};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3EEF491C-53D4-4614-B64C-1D98A699198C};; -Paikkatietoanalyysien tuloksia;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BBDCFF54-8109-4795-8BB5-15FA9480BB9C};Saatavuutta rajoitettu;{BBDCFF54-8109-4795-8BB5-15FA9480BB9C};{F039C4B7-7D3C-4EFA-99B0-C52091BC97B4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F039C4B7-7D3C-4EFA-99B0-C52091BC97B4};; -Patoturvallisuuden tietojärjestelmä PATOTURVA;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={895902E7-031C-48AD-A8AC-DCE7D325B9EE};Saatavuutta rajoitettu;{895902E7-031C-48AD-A8AC-DCE7D325B9EE};;;https://metadata.ymparisto.fi/dataset/;; -Pelastustoimen alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={75182F9D-D7D5-4A3A-8556-1C96577F0A52};Saatavuutta rajoitettu;{75182F9D-D7D5-4A3A-8556-1C96577F0A52};{34A6DAEE-DE66-45A2-B3A1-CB23CF6FF45E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{34A6DAEE-DE66-45A2-B3A1-CB23CF6FF45E};; -PINNAN AALTOEKSPOSITIO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB655D7F-1E25-4E19-AE7F-5DEBF1AEFC6C};Saatavuutta rajoitettu;{BB655D7F-1E25-4E19-AE7F-5DEBF1AEFC6C};{216DD75B-59A8-408A-80CD-F0785E548769};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{216DD75B-59A8-408A-80CD-F0785E548769};; -Pintavesien tilan tietojärjestelmä, vedenlaatu PIVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B1444E19-0F36-49F5-A849-01A3D2083A11};Saatavuutta rajoitettu;{B1444E19-0F36-49F5-A849-01A3D2083A11};;;https://metadata.ymparisto.fi/dataset/;; -Pohjaeläinhavaintoja POHJE-tietokannasta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2FA13650-7CB3-4299-8663-7542769D102E};Saatavuutta rajoitettu;{2FA13650-7CB3-4299-8663-7542769D102E};{0A22C866-3D0C-476A-9957-9275EB18B8AD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0A22C866-3D0C-476A-9957-9275EB18B8AD};; -Pohjaeläintietojärjestelmä - POHJE;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};Saatavuutta rajoitettu;{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};;https://metadata.ymparisto.fi/dataset/{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};; -Pohjavesitietojärjestelmä POVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C594F22E-B642-434D-B6F2-6ECB03F760C7};Saatavuutta rajoitettu;{C594F22E-B642-434D-B6F2-6ECB03F760C7};{C594F22E-B642-434D-B6F2-6ECB03F760C7};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C594F22E-B642-434D-B6F2-6ECB03F760C7};; -Poronhoidon paikkatiedot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={12E7100A-C010-40A3-A441-4EED54C9FC64};Saatavuutta rajoitettu;{12E7100A-C010-40A3-A441-4EED54C9FC64};{A6E6BD51-E463-4E0A-88EC-71DBFDF2223E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A6E6BD51-E463-4E0A-88EC-71DBFDF2223E};; -POTAMOGETON PERFOLIATUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D7F3685D-0432-4FC3-B0D4-13E798050A67};Saatavuutta rajoitettu;{D7F3685D-0432-4FC3-B0D4-13E798050A67};{8FD8F68C-373B-4A47-BB76-2E9040F3C488};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8FD8F68C-373B-4A47-BB76-2E9040F3C488};; -Protected Rapids;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9540CC1E-3E98-4C3D-A214-DC26A7DE6953};Saatavuutta rajoitettu;{9540CC1E-3E98-4C3D-A214-DC26A7DE6953};;;https://metadata.ymparisto.fi/dataset/;; -PUNALEVÄYHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={179A08D0-3B37-4A80-837E-83F151D5CDEA};Saatavuutta rajoitettu;{179A08D0-3B37-4A80-837E-83F151D5CDEA};;;https://metadata.ymparisto.fi/dataset/;; -Puolustusvoimien suoja-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={61D3FA7B-D8AC-40BA-A552-9FCED8F87375};Saatavuutta rajoitettu;{61D3FA7B-D8AC-40BA-A552-9FCED8F87375};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -PYLAIELLA LITTORALIS & ECTOCARPUS SILICULOSUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CF4921DE-7CD1-48C6-8C9C-1BFFC6662E14};Saatavuutta rajoitettu;{CF4921DE-7CD1-48C6-8C9C-1BFFC6662E14};{955901B1-C6D0-45CA-8EA6-C1EF95CA1C50};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{955901B1-C6D0-45CA-8EA6-C1EF95CA1C50};; -Rakennusperinnön hoitoavustushakemukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9D206B64-2EB7-47CC-AC9C-48459242444E};Saatavuutta rajoitettu;{9D206B64-2EB7-47CC-AC9C-48459242444E};{2CAEA52F-82FA-4E96-8E79-1327F27EF9A9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2CAEA52F-82FA-4E96-8E79-1327F27EF9A9};; -Rakennusperinnön inventointi kohteet ja alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4B84256C-0F01-456E-8A41-63231FC0D8F5};Saatavuutta rajoitettu;{4B84256C-0F01-456E-8A41-63231FC0D8F5};{D653718F-EE09-4F08-95B5-2AC42BAD5337};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D653718F-EE09-4F08-95B5-2AC42BAD5337};; -Rantalohkojako (SÖKÖ-toimintamalli);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={11A39DCD-4A35-4B90-AFBD-EEC3B778378C};Saatavuutta rajoitettu;{11A39DCD-4A35-4B90-AFBD-EEC3B778378C};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -River Basin Districts;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={65C4C67D-94F6-4B52-A656-069CC5853778};Saatavuutta rajoitettu;{65C4C67D-94F6-4B52-A656-069CC5853778};;;https://metadata.ymparisto.fi/dataset/;; -Ruoppaukset rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1A50B09C-2DEF-46C0-8664-19A3A5BF89CE};Saatavuutta rajoitettu;{1A50B09C-2DEF-46C0-8664-19A3A5BF89CE};{911BBDBD-5371-44E5-8AEA-3DEE2248AEE6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{911BBDBD-5371-44E5-8AEA-3DEE2248AEE6};; -Saaristomeren ja Selkämeren fladat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={45E434FE-36DF-431C-A9F2-D65C433F8751};Saatavuutta rajoitettu;{45E434FE-36DF-431C-A9F2-D65C433F8751};{FC7C4F23-81E4-4037-9DED-E45575205D30};Turun ammattikorkeakoulu;https://metadata.ymparisto.fi/dataset/{FC7C4F23-81E4-4037-9DED-E45575205D30};; -Sea Region Division;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={313F5784-71A0-46C2-A5B2-86AD9B17A4E8};Saatavuutta rajoitettu;{313F5784-71A0-46C2-A5B2-86AD9B17A4E8};;;https://metadata.ymparisto.fi/dataset/;; -Shoreline10 (Ranta10) and River network;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CED8AC3C-0651-4117-88A6-44CD05F6B42D};Saatavuutta rajoitettu;{CED8AC3C-0651-4117-88A6-44CD05F6B42D};;;https://metadata.ymparisto.fi/dataset/;; -SINISIMPUKKAYHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2F7CC7D6-380E-4E77-834F-7682EC6BC4FF};Saatavuutta rajoitettu;{2F7CC7D6-380E-4E77-834F-7682EC6BC4FF};;;https://metadata.ymparisto.fi/dataset/;; -STUCKENIA FILIFORMIS & STUCKENIA PECTINATA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BD151356-6EDB-4CB2-9BEF-E72D5AEBE936};Saatavuutta rajoitettu;{BD151356-6EDB-4CB2-9BEF-E72D5AEBE936};{BC774445-3D76-4F4F-B6A4-A63C631F0BAE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BC774445-3D76-4F4F-B6A4-A63C631F0BAE};; -Sukelluksissa havaittujen makroleväsukujen otannalla korjattu määrä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FF890247-EDD4-45E2-97BE-4570039870B7};Saatavuutta rajoitettu;{FF890247-EDD4-45E2-97BE-4570039870B7};{FBE17630-F6E1-47EA-B07E-C4A29BC33E3D};Metsähallitus;https://metadata.ymparisto.fi/dataset/{FBE17630-F6E1-47EA-B07E-C4A29BC33E3D};; -SYKEn hakemistorajapinta (OData);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3563384E-DF21-49F6-9E0A-CCB1B8A74A44};Saatavuutta rajoitettu;{3563384E-DF21-49F6-9E0A-CCB1B8A74A44};{318D5E0E-7E13-47F2-93E7-04DFE2A12917};Suomen ympäristökeskus / Tietokeskus;https://metadata.ymparisto.fi/dataset/{318D5E0E-7E13-47F2-93E7-04DFE2A12917};; -Säähavaintoasemat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D2BF7AA2-6E0A-4EFF-BB77-1EA5330A3518};Saatavuutta rajoitettu;{D2BF7AA2-6E0A-4EFF-BB77-1EA5330A3518};{CDE7624A-AA3A-4592-B086-24008F05108D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CDE7624A-AA3A-4592-B086-24008F05108D};; -SÖKÖ-karttalehdet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3A7B5ECF-5A1A-4CE8-8880-EFA161B0452E};Saatavuutta rajoitettu;{3A7B5ECF-5A1A-4CE8-8880-EFA161B0452E};{D925EC77-C5F0-4D1E-A691-93ED0FACDAD6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D925EC77-C5F0-4D1E-A691-93ED0FACDAD6};; -Teollisuus rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BDBE0944-5B2C-4CE8-A404-34B8292DF808};Saatavuutta rajoitettu;{BDBE0944-5B2C-4CE8-A404-34B8292DF808};{8D033A6C-D9A7-4622-B518-E4358303BD2C};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{8D033A6C-D9A7-4622-B518-E4358303BD2C};; -Tieriskirekisteri TSRR;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8047A089-C13F-4729-920B-3C18F471F711};Saatavuutta rajoitettu;{8047A089-C13F-4729-920B-3C18F471F711};{47A9B147-CC81-460F-976B-E3A2A756D15A};Pohjavesiaineisto- SYKE, Tieaineisto- LiV;https://metadata.ymparisto.fi/dataset/{47A9B147-CC81-460F-976B-E3A2A756D15A};; -Total Suspended Matter, FI-LAKES, 2012-06-20;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=e98cabc7-65ad-4e72-a6d6-f42e2cc591da;Saatavuutta rajoitettu;e98cabc7-65ad-4e72-a6d6-f42e2cc591da;;;https://metadata.ymparisto.fi/dataset/;; -Total suspended matter, FI-SOUTH, 2009-09-15;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=69b7df80-3d67-11e2-941e-005056c00008;Saatavuutta rajoitettu;69b7df80-3d67-11e2-941e-005056c00008;;;https://metadata.ymparisto.fi/dataset/;; -Traffic and Water Ways' Restriction Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A8126E3C-8F96-43C3-883D-BF315E38284E};Saatavuutta rajoitettu;{A8126E3C-8F96-43C3-883D-BF315E38284E};;;https://metadata.ymparisto.fi/dataset/;; -Traffic Risk Zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F1E7CB96-6CA3-4E51-B057-464AF658C46E};Saatavuutta rajoitettu;{F1E7CB96-6CA3-4E51-B057-464AF658C46E};;;https://metadata.ymparisto.fi/dataset/;; -Tulvariskiruudut;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7F0ABF2F-A175-4D9A-A04B-C6514B94D082};Saatavuutta rajoitettu;{7F0ABF2F-A175-4D9A-A04B-C6514B94D082};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Tulvatietojärjestelmä TULVATJ;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={58AAFEFC-1429-4343-8EE2-F5E136E0A276};Saatavuutta rajoitettu;{58AAFEFC-1429-4343-8EE2-F5E136E0A276};{58AAFEFC-1429-4343-8EE2-F5E136E0A276};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{58AAFEFC-1429-4343-8EE2-F5E136E0A276};; -Tuottajavastuutietojärjestelmä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0E73BB28-1574-48A0-87CA-FD4BC1211896};Saatavuutta rajoitettu;{0E73BB28-1574-48A0-87CA-FD4BC1211896};;;https://metadata.ymparisto.fi/dataset/;; -Uomatietojärjestelmä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FB936A19-421D-486D-95AC-009BD53AA1B2};Saatavuutta rajoitettu;{FB936A19-421D-486D-95AC-009BD53AA1B2};{FB936A19-421D-486D-95AC-009BD53AA1B2};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{FB936A19-421D-486D-95AC-009BD53AA1B2};; -Valtakunnallinen maakuntakaavapaikkatietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8E578155-1FD3-4B69-B6EB-0AB18B8F6FE6};Saatavuutta rajoitettu;{8E578155-1FD3-4B69-B6EB-0AB18B8F6FE6};{82174E6B-DBC6-439F-A0F0-B425C8FBB28F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{82174E6B-DBC6-439F-A0F0-B425C8FBB28F};; -Valtakunnallisen yöperhosseurannan keskustietokanta YÖPETI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1FF762A2-EE3F-415C-9895-D866B2E8A9CF};Saatavuutta rajoitettu;{1FF762A2-EE3F-415C-9895-D866B2E8A9CF};{4CA7BB48-01F0-4544-B153-E17569155FBD};SYKE;https://metadata.ymparisto.fi/dataset/{4CA7BB48-01F0-4544-B153-E17569155FBD};; -Valtion öljyntorjuntavarastot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3E35CBD1-1C57-4E17-96EC-6E41256E4619};Saatavuutta rajoitettu;{3E35CBD1-1C57-4E17-96EC-6E41256E4619};{872D9B86-1902-4343-999C-2F76348C4524};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{872D9B86-1902-4343-999C-2F76348C4524};; -Value-valuma-aluejako (ehdotus);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={31B11BD6-7886-447C-8A24-2B8FFE6B2D07};Saatavuutta rajoitettu;{31B11BD6-7886-447C-8A24-2B8FFE6B2D07};{E848DF66-C3A7-47E5-956A-5FEBBDBD924B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E848DF66-C3A7-47E5-956A-5FEBBDBD924B};; -Valvonta ja kuormitustietojärjestelmä VAHTI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D1539BE1-7A48-4CC8-AEC9-B32C7650A399};Saatavuutta rajoitettu;{D1539BE1-7A48-4CC8-AEC9-B32C7650A399};;;https://metadata.ymparisto.fi/dataset/;; -Vanhan rakennuslain mukaisesti vahvistettujen yleiskaavojen rasterit;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1377662D-F6F1-48F7-86A0-ADD001F08CE1};Saatavuutta rajoitettu;{1377662D-F6F1-48F7-86A0-ADD001F08CE1};{970599E7-8793-47A6-BF67-23E845E82ED1};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{970599E7-8793-47A6-BF67-23E845E82ED1};; -Vanhan rakennuslain mukaisesti vahvistettujen yleiskaavojen ulkorajaukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E200F2D-7983-42C1-84AF-459FD31AB4EC};Saatavuutta rajoitettu;{1E200F2D-7983-42C1-84AF-459FD31AB4EC};{AAFD1646-16FC-42D8-9F1B-DA8272A9D062};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AAFD1646-16FC-42D8-9F1B-DA8272A9D062};; -Varsinais-Suomen veneluiskat 2016;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AFB2C26E-E63B-4324-8E06-3B806EC37C92};Saatavuutta rajoitettu;{AFB2C26E-E63B-4324-8E06-3B806EC37C92};{5E45B067-25E6-41F2-8129-DD7C2BF67148};Turun ammattikorkeakoulu Oy;https://metadata.ymparisto.fi/dataset/{5E45B067-25E6-41F2-8129-DD7C2BF67148};; -Varsinais-Suomen väylien riskikartoitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={00DEEA25-2286-49B4-AED4-CB20A1BA5B9E};Saatavuutta rajoitettu;{00DEEA25-2286-49B4-AED4-CB20A1BA5B9E};{AB829531-5139-4A9D-9CF6-162AE62B27D0};Turun ammattikorkeakoulu;https://metadata.ymparisto.fi/dataset/{AB829531-5139-4A9D-9CF6-162AE62B27D0};; -VELMU kartoitusmenetelmät;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7F62E2AC-7C1B-4B3F-A66A-8BE03E01388B};Saatavuutta rajoitettu;{7F62E2AC-7C1B-4B3F-A66A-8BE03E01388B};{2BED2EB3-CDAA-4071-ADD3-1FC081C23440};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2BED2EB3-CDAA-4071-ADD3-1FC081C23440};; -VELMU kuvapisteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FBE41DF9-F410-4490-B9B6-8418CB33617E};Saatavuutta rajoitettu;{FBE41DF9-F410-4490-B9B6-8418CB33617E};{4504165A-74A4-4675-B95A-FA5274C4984A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{4504165A-74A4-4675-B95A-FA5274C4984A};; -VELMU lajihavainnot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D03358DD-CA34-487F-8F85-03A74C9D7296};Saatavuutta rajoitettu;{D03358DD-CA34-487F-8F85-03A74C9D7296};{C3C7C925-5872-4C30-A824-8F723BE3ABC8};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C3C7C925-5872-4C30-A824-8F723BE3ABC8};; -VELMU lajihavainnot (uhanalaiset lajit);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F500A951-A177-4CAA-83C5-FC54F2255FAB};Saatavuutta rajoitettu;{F500A951-A177-4CAA-83C5-FC54F2255FAB};{BF30DFF5-B5F8-4D8D-BAF7-D3671BBE432A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF30DFF5-B5F8-4D8D-BAF7-D3671BBE432A};; -Velmu taustakartta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B2B2A595-1225-4E18-B49C-EEBB5A6C5CE3};Saatavuutta rajoitettu;{B2B2A595-1225-4E18-B49C-EEBB5A6C5CE3};{60776441-5F9D-43EB-B902-A30CFBE89C2A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{60776441-5F9D-43EB-B902-A30CFBE89C2A};; -Venesatamat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4166A97E-E513-4B69-B498-447A8FA71B9A};Saatavuutta rajoitettu;{4166A97E-E513-4B69-B498-447A8FA71B9A};{D4C89F91-AA18-4359-B1D6-42C9C436332A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D4C89F91-AA18-4359-B1D6-42C9C436332A};; -Vesienhoitoalueiden pintavesien kaikki seurantapaikat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={356F0345-ABFE-4245-8231-BE4BFA83C541};Saatavuutta rajoitettu;{356F0345-ABFE-4245-8231-BE4BFA83C541};{79CF5A64-2690-4BF1-83C9-66C9CF8C041D};Suomen ympäristökeskus/Vesikeskus;https://metadata.ymparisto.fi/dataset/{79CF5A64-2690-4BF1-83C9-66C9CF8C041D};; -Vesienhoitoalueiden seuranta - VHS Seuranta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F2FAD430-3632-4ACC-B2B1-70680795361E};Saatavuutta rajoitettu;{F2FAD430-3632-4ACC-B2B1-70680795361E};{F2FAD430-3632-4ACC-B2B1-70680795361E};Suomen ympäristökeskus / Vesikeskus / Sisävesiyksikkö;https://metadata.ymparisto.fi/dataset/{F2FAD430-3632-4ACC-B2B1-70680795361E};; -vesienhoitoalueiden seurantapaikat, Pohjavesi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3E07BDDC-AB68-4CDB-A647-BA6FCA784AB3};Saatavuutta rajoitettu;{3E07BDDC-AB68-4CDB-A647-BA6FCA784AB3};{5168AC0A-DD96-4231-9B4A-26DEAC055DC4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5168AC0A-DD96-4231-9B4A-26DEAC055DC4};; -Vesienhoitoalueiden seurantapaikat. Pintavesi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={22E6CAF3-20A9-44D1-B054-E019FEF882D4};Saatavuutta rajoitettu;{22E6CAF3-20A9-44D1-B054-E019FEF882D4};;;https://metadata.ymparisto.fi/dataset/;; -Vesihuollon tietojärjestelmä VEETI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F363ACA2-6CF5-46EF-907C-8F6CAF50C17B};Saatavuutta rajoitettu;{F363ACA2-6CF5-46EF-907C-8F6CAF50C17B};{373B7BEC-F23D-4AEF-9194-A646006F95AD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{373B7BEC-F23D-4AEF-9194-A646006F95AD};; -Vesihuoltolaitosten tilastointijärjestelmä VELVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DA001EC6-DB26-4339-AB97-DAB8FC2968B3};Saatavuutta rajoitettu;{DA001EC6-DB26-4339-AB97-DAB8FC2968B3};;;https://metadata.ymparisto.fi/dataset/;; -Vesihuoltoverkoston potentiaali (jätevesi, talousvesi ja jätevesi);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={479C025B-3ECC-4E97-8FF0-23E82B5A521B};Saatavuutta rajoitettu;{479C025B-3ECC-4E97-8FF0-23E82B5A521B};{363584D7-C854-4A20-BD7E-A1385B4E171D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{363584D7-C854-4A20-BD7E-A1385B4E171D};; -Vesiliikennerajoitukset-karttapalvelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2AD52986-F67E-4417-B733-BF57C94B4F02};Saatavuutta rajoitettu;{2AD52986-F67E-4417-B733-BF57C94B4F02};{5E05F2A7-17C5-44D3-99E2-68C263D3DD32};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5E05F2A7-17C5-44D3-99E2-68C263D3DD32};; -Vesimuodostumat-tietojärjestelmä VEMU;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6CD780AB-790D-4DA8-8962-9C8186B4127D};Saatavuutta rajoitettu;{6CD780AB-790D-4DA8-8962-9C8186B4127D};{6CD780AB-790D-4DA8-8962-9C8186B4127D};Suomen ympäristökeskus / Vesikeskus / Vesivarayksikkö;https://metadata.ymparisto.fi/dataset/{6CD780AB-790D-4DA8-8962-9C8186B4127D};; -Vesistömallijärjestelmä (WSFS-VEMALA);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C08C0416-6605-4BC5-8956-58AAF95F0030};Saatavuutta rajoitettu;{C08C0416-6605-4BC5-8956-58AAF95F0030};;;https://metadata.ymparisto.fi/dataset/;; -Vesistötyöt VESTY - Vesistöhankkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CA4578AC-3B2E-488F-8A0D-C91E470DAE49};Saatavuutta rajoitettu;{CA4578AC-3B2E-488F-8A0D-C91E470DAE49};{789B889E-6D94-413F-BD07-F811137637B7};Lasse Järvenpää;https://metadata.ymparisto.fi/dataset/{789B889E-6D94-413F-BD07-F811137637B7};; -Vesistötyöt VESTY - Rakenteet ja toimenpiteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={54CB6655-3038-4FAC-9822-5BADA622570F};Saatavuutta rajoitettu;{54CB6655-3038-4FAC-9822-5BADA622570F};{C13F44E5-E626-4AE5-BD23-EDCC1A21CDA9};Suomen ympäristökeskus / Vesikeskus / Vesivarayksikkö;https://metadata.ymparisto.fi/dataset/{C13F44E5-E626-4AE5-BD23-EDCC1A21CDA9};; -Vesivarat - käyttötoiminta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6F7ADB6E-193B-47A2-A4F6-2320D66D706A};Saatavuutta rajoitettu;{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};; -Vesiviljelyn sijainninohjaussuunnitelma;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9B1F82F-D696-49A1-9CC8-B69EEE5D61FF};Saatavuutta rajoitettu;{E9B1F82F-D696-49A1-9CC8-B69EEE5D61FF};;Maa- ja metsätalousministeriö, Ruokavirasto;https://metadata.ymparisto.fi/dataset/;; -Väestötietojärjestelmän rakennus- ja huoneistotiedot RHR;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={18C8309E-3963-4738-B6D2-96BA825344DC};Saatavuutta rajoitettu;{18C8309E-3963-4738-B6D2-96BA825344DC};;Digi- ja väestötietovirasto;https://metadata.ymparisto.fi/dataset/;; -Vähittäiskaupan toimipaikat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2A5BD858-ECAB-4E8B-B3B1-7FD0FE0DA798};Saatavuutta rajoitettu;{2A5BD858-ECAB-4E8B-B3B1-7FD0FE0DA798};;Tilastokeskus;https://metadata.ymparisto.fi/dataset/;; -Yleiskaavarekisteri - Yleiskaava MOPO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={34E2147E-D704-408D-A053-F91EEE20F94F};Saatavuutta rajoitettu;{34E2147E-D704-408D-A053-F91EEE20F94F};;;https://metadata.ymparisto.fi/dataset/;; -Ympäristöhallinnon kirjastojen kokoelma- ja artikkelitietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={51D1BC1D-93E5-4369-8019-630ACDEF53BA};Saatavuutta rajoitettu;{51D1BC1D-93E5-4369-8019-630ACDEF53BA};{51D1BC1D-93E5-4369-8019-630ACDEF53BA};Suomen ympäristökeskus / Tietokeskus / Tietopalvelu ja kirjasto;https://metadata.ymparisto.fi/dataset/{51D1BC1D-93E5-4369-8019-630ACDEF53BA};; -Ympäristöhallinnon kirjastojen lehtitietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B313B6FD-1CAB-41DD-9EF8-CF602F361773};Saatavuutta rajoitettu;{B313B6FD-1CAB-41DD-9EF8-CF602F361773};{B313B6FD-1CAB-41DD-9EF8-CF602F361773};Suomen ympäristökeskus / Tietokeskus / Tietopalvelu ja kirjasto;https://metadata.ymparisto.fi/dataset/{B313B6FD-1CAB-41DD-9EF8-CF602F361773};; -Ympäristökarttapalvelu karpalo;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7FB975C-846E-451A-9F5E-00EE3D2383AA};Saatavuutta rajoitettu;{B7FB975C-846E-451A-9F5E-00EE3D2383AA};{1334BB17-D27D-4876-9362-EF59256CC6BD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{1334BB17-D27D-4876-9362-EF59256CC6BD};; -Zostera marina levinneisyyysmalli;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CA32C9F7-7475-4EFE-8E1C-22B1FB96F008};Saatavuutta rajoitettu;{CA32C9F7-7475-4EFE-8E1C-22B1FB96F008};{30F193DB-2CD7-4DC1-8A31-84CB06B16213};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{30F193DB-2CD7-4DC1-8A31-84CB06B16213};; \ No newline at end of file From f098d9c875e569d5247f70c14b58567f22ce5802 Mon Sep 17 00:00:00 2001 From: Toni Nurmi Date: Thu, 28 Jan 2021 13:17:46 +0200 Subject: [PATCH 05/21] Merge Test to Stable (#737) (#746) [FIX] Bug where files could not be added to drafts #742 [FIX] Remove test comparing datetime objects #744 [FIX] Draft validation issues #740 [ADD] Support for DOI as preferred identifier #741 [ADD] Support for bulk-delete api operations #725 [ADD] Enable V2 endpoints --- .flake8 | 0 .github/dependabot.yml | 0 .gitignore | 5 + .travis.yml | 60 +- AUTHORS | 0 Dockerfile | 33 + ENV_VARS.md | 62 + LICENSE | 0 README.md | 123 + containers/portainer/templates.json | 234 + deploy-key.enc | Bin docker-compose.yml | 34 + docs/README.md | 0 docs/v1/source/_static/custom.css | 0 docs/v1/source/conf.py | 0 docs/v1/source/datasets.rst | 0 docs/v1/source/end_users.rst | 0 docs/v1/source/files.rst | 0 docs/v1/source/general.rst | 0 docs/v1/source/index.rst | 0 docs/v1/source/list_operations.rst | 0 docs/v1/source/metax_api.rst | 0 docs/v1/source/metax_high_level.png | Bin docs/v1/source/metax_high_level.xml | 0 docs/v1/source/oai_pmh.rst | 0 docs/v1/source/quick_start.rst | 0 docs/v1/source/reference_data.rst | 0 docs/v2/source/_static/custom.css | 0 docs/v2/source/conf.py | 0 docs/v2/source/datasets.rst | 0 docs/v2/source/end_users.rst | 0 docs/v2/source/files.rst | 0 docs/v2/source/general.rst | 0 docs/v2/source/index.rst | 0 docs/v2/source/list_operations.rst | 0 docs/v2/source/metax_api.rst | 0 docs/v2/source/metax_high_level.png | Bin docs/v2/source/metax_high_level.xml | 0 docs/v2/source/oai_pmh.rst | 0 docs/v2/source/quick_start.rst | 0 docs/v2/source/reference_data.rst | 0 requirements.in | 44 +- requirements.txt | 23 +- resources/sql/init_test.sql | 0 src/.coveragerc | 0 src/metax_api/__init__.py | 0 src/metax_api/api/__init__.py | 0 .../api/oaipmh/base/metax_oai_server.py | 2 +- src/metax_api/api/oaipmh/base/view.py | 5 +- src/metax_api/api/rest/base/__init__.py | 0 .../rest/base/api_schemas/catalogrecord.json | 0 .../rest/base/api_schemas/datacatalog.json | 0 .../api/rest/base/api_schemas/file.json | 0 .../api/rest/base/schemas/__init__.py | 0 .../rest/base/schemas/att_dataset_schema.json | 0 .../rest/base/schemas/contract_schema.json | 0 .../rest/base/schemas/datacatalog_schema.json | 0 .../base/schemas/datacite_4.1_schema.json | 0 .../api/rest/base/schemas/file_schema.json | 0 .../schemas/harvester_dataset_schema.json | 0 .../rest/base/schemas/ida_dataset_schema.json | 0 .../api/rest/base/serializers/__init__.py | 0 .../serializers/catalog_record_serializer.py | 1 - .../base/serializers/common_serializer.py | 7 +- .../base/serializers/contract_serializer.py | 0 .../serializers/data_catalog_serializer.py | 0 .../base/serializers/directory_serializer.py | 2 +- .../rest/base/serializers/file_serializer.py | 3 +- .../serializers/file_storage_serializer.py | 0 .../rest/base/serializers/serializer_utils.py | 0 .../serializers/xml_metadata_serializer.py | 0 src/metax_api/api/rest/base/views/__init__.py | 2 +- .../api/rest/base/views/api_error_view.py | 0 .../api/rest/base/views/common_view.py | 2 +- .../api/rest/base/views/contract_view.py | 0 .../api/rest/base/views/data_catalog_view.py | 0 .../api/rest/base/views/dataset_view.py | 305 +- .../api/rest/base/views/directory_view.py | 0 .../api/rest/base/views/file_storage_view.py | 0 .../api/rest/base/views/file_view.py | 1 - .../api/rest/base/views/schema_view.py | 0 .../api/rest/base/xslt/datacite.xslt | 0 src/metax_api/api/rest/v2/__init__.py | 0 .../rest/v2/api_schemas/catalogrecord.json | 0 .../api/rest/v2/api_schemas/datacatalog.json | 0 .../v2/api_schemas/dataset_files_schema.json | 0 .../api/rest/v2/api_schemas/file.json | 0 .../v2/api_schemas/ida_dataset_schema.json | 0 src/metax_api/api/rest/v2/schemas/__init__.py | 0 .../rest/v2/schemas/att_dataset_schema.json | 0 .../api/rest/v2/schemas/contract_schema.json | 0 .../rest/v2/schemas/datacatalog_schema.json | 0 .../rest/v2/schemas/datacite_4.1_schema.json | 0 .../rest/v2/schemas/dataset_files_schema.json | 0 .../rest/v2/schemas/dft_dataset_schema.json | 239 + .../api/rest/v2/schemas/file_schema.json | 0 .../v2/schemas/harvester_dataset_schema.json | 0 .../rest/v2/schemas/ida_dataset_schema.json | 0 .../api/rest/v2/serializers/__init__.py | 0 .../serializers/catalog_record_serializer.py | 21 +- .../v2/serializers/data_catalog_serializer.py | 0 src/metax_api/api/rest/v2/views/__init__.py | 2 +- .../api/rest/v2/views/data_catalog_view.py | 0 .../api/rest/v2/views/dataset_view.py | 6 +- src/metax_api/api/rpc/base/__init__.py | 0 src/metax_api/api/rpc/base/views/__init__.py | 2 +- .../api/rpc/base/views/common_rpc.py | 4 +- .../api/rpc/base/views/dataset_rpc.py | 2 +- .../api/rpc/base/views/elasticsearch_rpc.py | 2 +- src/metax_api/api/rpc/base/views/file_rpc.py | 2 +- .../api/rpc/base/views/statistic_rpc.py | 3 +- src/metax_api/api/rpc/v2/__init__.py | 0 src/metax_api/api/rpc/v2/router.py | 1 - src/metax_api/api/rpc/v2/views/__init__.py | 0 src/metax_api/api/rpc/v2/views/dataset_rpc.py | 3 +- src/metax_api/checks.py | 47 + .../exampledata/dataset_minimal.json | 0 src/metax_api/exceptions/__init__.py | 0 src/metax_api/exceptions/http_exceptions.py | 0 src/metax_api/initialdata/datacatalogs.json | 2 +- src/metax_api/initialdata/filestorages.json | 0 src/metax_api/management/__init__.py | 0 src/metax_api/management/commands/__init__.py | 0 .../management/commands/index_refdata.py | 8 + .../management/commands/loadinitialdata.py | 123 +- .../commands/reload_refdata_cache.py | 21 + .../commands/updatereferencedata.py | 12 +- src/metax_api/middleware/__init__.py | 2 +- .../add_last_modified_header_to_response.py | 24 +- src/metax_api/middleware/identifyapicaller.py | 107 +- src/metax_api/middleware/request_logging.py | 127 +- .../middleware/stream_http_response.py | 40 +- src/metax_api/migrations/0001_initial.py | 0 .../migrations/0002_create_json_indexes.py | 0 .../migrations/0003_auto_20180830_1534.py | 0 .../migrations/0004_auto_20180919_1502.py | 0 .../migrations/0005_auto_20181101_1524.py | 0 .../0006_catalogrecord_date_deprecated.py | 0 .../migrations/0007_auto_20181126_1436.py | 0 .../0008_remove_file_replication_path.py | 0 ...9_catalogrecord_preservation_identifier.py | 0 .../migrations/0010_auto_20190627_1032.py | 0 ...alogrecord_preservation_dataset_version.py | 0 .../migrations/0012_auto_20190823_1055.py | 0 .../0013_catalogrecord__access_granter.py | 0 .../migrations/0014_catalogrecord_state.py | 0 .../migrations/0015_auto_20200218_1607.py | 0 .../migrations/0016_auto_20200310_1341.py | 0 .../0017_catalogrecord_rems_identifier.py | 0 .../migrations/0018_auto_20200330_1101.py | 0 .../migrations/0019_auto_20200419_2149.py | 0 .../migrations/0020_auto_20200429_1512.py | 0 .../0021_catalogrecord_next_draft.py | 0 .../migrations/0022_auto_20200528_1758.py | 0 .../migrations/0023_catalogrecord_api_meta.py | 0 ...024_catalogrecord_use_doi_for_published.py | 0 .../migrations/0025_auto_20200811_1050.py | 0 .../migrations/0026_auto_20200917_1615.py | 0 .../migrations/0027_auto_20201013_1404.py | 41 +- .../migrations/0028_auto_20201020_1010.py | 0 .../migrations/0029_auto_20201028_1602.py | 0 src/metax_api/migrations/__init__.py | 0 .../migrations/datasets/SYKE_csv.csv | 478 +- src/metax_api/models/__init__.py | 0 src/metax_api/models/catalog_record.py | 24 +- src/metax_api/models/catalog_record_v2.py | 32 +- src/metax_api/models/common.py | 0 src/metax_api/models/contract.py | 2 +- src/metax_api/models/data_catalog.py | 0 src/metax_api/models/directory.py | 3 +- src/metax_api/models/file.py | 0 src/metax_api/models/file_storage.py | 1 + src/metax_api/models/metax_user.py | 0 src/metax_api/models/xml_metadata.py | 0 src/metax_api/onappstart.py | 69 +- src/metax_api/parsers/__init__.py | 0 src/metax_api/parsers/parsers.py | 0 src/metax_api/permissions/__init__.py | 0 src/metax_api/permissions/permissions.py | 1 - src/metax_api/renderers/__init__.py | 0 src/metax_api/renderers/renderers.py | 0 src/metax_api/services/__init__.py | 5 +- src/metax_api/services/api_error_service.py | 4 +- src/metax_api/services/auth_service.py | 3 +- src/metax_api/services/callable_service.py | 0 .../services/catalog_record_service.py | 42 +- .../services/catalog_record_service_v2.py | 1 - src/metax_api/services/common_service.py | 19 +- .../services/data_catalog_service.py | 6 +- src/metax_api/services/datacite_service.py | 6 +- src/metax_api/services/file_service.py | 5 +- src/metax_api/services/pagination.py | 0 src/metax_api/services/rabbitmq_service.py | 156 +- src/metax_api/services/redis_cache_service.py | 207 +- .../services/reference_data_mixin.py | 4 + src/metax_api/services/rems_service.py | 3 +- src/metax_api/services/schema_service.py | 5 +- src/metax_api/services/statistic_service.py | 1 - src/metax_api/settings.py | 654 -- src/metax_api/settings/.env.template | 16 + src/metax_api/settings/__init__.py | 78 + src/metax_api/settings/components/__init__.py | 3 + .../settings/components/access_control.py | 71 + src/metax_api/settings/components/common.py | 196 + .../settings/components/elasticsearch.py | 13 + .../settings/components/externals.py | 28 + src/metax_api/settings/components/logging.py | 88 + src/metax_api/settings/components/rabbitmq.py | 31 + src/metax_api/settings/components/redis.py | 34 + src/metax_api/settings/components/rems.py | 20 + .../settings/environments/__init__.py | 0 src/metax_api/settings/environments/local.py | 49 + src/metax_api/settings/environments/remote.py | 297 + src/metax_api/settings/environments/test.py | 80 + src/metax_api/settings/environments/travis.py | 9 + src/metax_api/tasks/__init__.py | 0 src/metax_api/tasks/refdata/__init__.py | 0 .../tasks/refdata/refdata_indexer/__init__.py | 3 + .../refdata_indexer/domain/__init__.py | 3 + .../refdata_indexer/domain/indexable_data.py | 50 + .../domain/organization_data.py | 47 + .../refdata_indexer/domain/reference_data.py | 119 + .../refdata/refdata_indexer/es_index_data.py | 106 + .../refdata/refdata_indexer/logconf.json | 24 + .../organization_csv_parser.py | 134 + .../refdata/refdata_indexer/requirements.txt | 3 + .../es-config/organization_data_index.json | 57 + .../es-config/reference_data_index.json | 75 + .../resources/local-refdata/access_type.json | 7 + .../local-refdata/contributor_role.json | 14 + .../local-refdata/contributor_type.json | 20 + .../local-refdata/event_outcome.json | 3 + .../local-refdata/file_format_version.json | 100 + .../resources/local-refdata/file_type.json | 9 + .../resources/local-refdata/funder_type.json | 13 + .../local-refdata/identifier_type.json | 21 + .../resources/local-refdata/license.json | 308 + .../local-refdata/lifecycle_event.json | 16 + .../local-refdata/preservation_event.json | 21 + .../local-refdata/relation_type.json | 14 + .../local-refdata/research_infra.json | 50 + .../local-refdata/resource_type.json | 17 + .../local-refdata/restriction_grounds.json | 13 + .../resources/local-refdata/use_category.json | 8 + .../resources/organizations/organizations.csv | 2440 +++++++ .../refdata_indexer/resources/uri_to_wkt.json | 5980 +++++++++++++++++ .../refdata_indexer/service/__init__.py | 3 + .../service/elasticsearch_service.py | 111 + .../service/finto_data_service.py | 231 + .../service/infra_data_service.py | 94 + .../service/local_data_service.py | 48 + .../service/mime_data_service.py | 132 + .../service/organization_service.py | 43 + .../refdata_indexer/service/service_utils.py | 18 + .../templates/secure/auth_success.html | 0 src/metax_api/tests/__init__.py | 0 src/metax_api/tests/api/__init__.py | 0 src/metax_api/tests/api/oaipmh/__init__.py | 0 src/metax_api/tests/api/oaipmh/minimal_api.py | 2 +- src/metax_api/tests/api/oaipmh/syke.py | 6 +- src/metax_api/tests/api/rest/__init__.py | 0 src/metax_api/tests/api/rest/base/__init__.py | 0 .../api/rest/base/serializers/__init__.py | 0 .../rest/base/serializers/file_serializer.py | 2 +- .../rest/base/serializers/serializer_utils.py | 1 - .../tests/api/rest/base/views/__init__.py | 0 .../api/rest/base/views/apierrors/__init__.py | 0 .../api/rest/base/views/apierrors/read.py | 161 +- .../api/rest/base/views/common/__init__.py | 0 .../tests/api/rest/base/views/common/auth.py | 102 +- .../tests/api/rest/base/views/common/read.py | 0 .../tests/api/rest/base/views/common/write.py | 1 - .../api/rest/base/views/contracts/__init__.py | 0 .../rest/base/views/contracts/contracts.py | 0 .../rest/base/views/datacatalogs/__init__.py | 0 .../api/rest/base/views/datacatalogs/read.py | 2 +- .../api/rest/base/views/datacatalogs/write.py | 3 +- .../api/rest/base/views/datasets/__init__.py | 0 .../api/rest/base/views/datasets/read.py | 4 +- .../api/rest/base/views/datasets/write.py | 102 +- .../rest/base/views/directories/__init__.py | 0 .../api/rest/base/views/directories/read.py | 4 +- .../api/rest/base/views/directories/write.py | 0 .../api/rest/base/views/files/__init__.py | 0 .../tests/api/rest/base/views/files/read.py | 2 +- .../tests/api/rest/base/views/files/write.py | 7 +- .../rest/base/views/filestorages/__init__.py | 0 .../api/rest/base/views/filestorages/read.py | 0 .../api/rest/base/views/filestorages/write.py | 0 .../api/rest/base/views/schemas/__init__.py | 0 .../tests/api/rest/base/views/schemas/read.py | 4 +- src/metax_api/tests/api/rest/v2/__init__.py | 0 .../tests/api/rest/v2/views/__init__.py | 0 .../api/rest/v2/views/apierrors/__init__.py | 0 .../tests/api/rest/v2/views/apierrors/read.py | 0 .../api/rest/v2/views/common/__init__.py | 0 .../tests/api/rest/v2/views/common/auth.py | 101 +- .../tests/api/rest/v2/views/common/read.py | 0 .../tests/api/rest/v2/views/common/write.py | 1 - .../api/rest/v2/views/contracts/__init__.py | 0 .../api/rest/v2/views/contracts/contracts.py | 0 .../rest/v2/views/datacatalogs/__init__.py | 0 .../api/rest/v2/views/datacatalogs/read.py | 0 .../api/rest/v2/views/datacatalogs/write.py | 3 +- .../api/rest/v2/views/datasets/__init__.py | 7 +- .../v2/views/datasets/api_version_lock.py | 0 .../api/rest/v2/views/datasets/drafts.py | 49 +- .../rest/v2/views/datasets/filehandling.py | 1 - .../tests/api/rest/v2/views/datasets/pas.py | 0 .../tests/api/rest/v2/views/datasets/read.py | 4 +- .../rest/v2/views/datasets/referencedata.py | 5 +- .../tests/api/rest/v2/views/datasets/rems.py | 3 +- .../tests/api/rest/v2/views/datasets/write.py | 134 +- .../api/rest/v2/views/directories/__init__.py | 0 .../api/rest/v2/views/directories/read.py | 4 +- .../api/rest/v2/views/directories/write.py | 0 .../tests/api/rest/v2/views/files/__init__.py | 0 .../tests/api/rest/v2/views/files/read.py | 2 +- .../tests/api/rest/v2/views/files/write.py | 7 +- .../rest/v2/views/filestorages/__init__.py | 0 .../api/rest/v2/views/filestorages/read.py | 0 .../api/rest/v2/views/filestorages/write.py | 0 .../api/rest/v2/views/schemas/__init__.py | 0 .../tests/api/rest/v2/views/schemas/read.py | 4 +- src/metax_api/tests/api/rpc/__init__.py | 0 src/metax_api/tests/api/rpc/base/__init__.py | 0 .../tests/api/rpc/base/views/__init__.py | 0 .../tests/api/rpc/base/views/common_rpc.py | 0 .../tests/api/rpc/base/views/dataset_rpc.py | 28 +- .../tests/api/rpc/base/views/file_rpc.py | 2 +- .../tests/api/rpc/base/views/statistic_rpc.py | 0 src/metax_api/tests/api/rpc/v2/__init__.py | 0 .../tests/api/rpc/v2/views/__init__.py | 0 .../tests/api/rpc/v2/views/common_rpc.py | 0 .../tests/api/rpc/v2/views/dataset_rpc.py | 27 +- .../tests/api/rpc/v2/views/file_rpc.py | 2 +- .../tests/api/rpc/v2/views/statistic_rpc.py | 0 src/metax_api/tests/management/__init__.py | 0 .../tests/management/commands/__init__.py | 0 .../management/commands/loadinitialdata.py | 0 src/metax_api/tests/middleware/__init__.py | 0 .../tests/middleware/test_middleware.py | 2 +- .../tests/middleware/test_middleware_v2.py | 2 +- src/metax_api/tests/models/__init__.py | 0 src/metax_api/tests/models/catalog_record.py | 0 src/metax_api/tests/models/common.py | 0 src/metax_api/tests/models/data_catalog.py | 0 src/metax_api/tests/models/directory.py | 2 +- src/metax_api/tests/models/file.py | 0 src/metax_api/tests/rabbitmq/consume.py | 9 +- src/metax_api/tests/services/__init__.py | 0 .../tests/services/reference_data_mixin.py | 16 +- src/metax_api/tests/testdata/README.md | 0 src/metax_api/tests/testdata/__init__.py | 0 .../catalog_record_test_data_template.json | 0 ...og_record_test_data_template_full_att.json | 0 ...og_record_test_data_template_full_ida.json | 0 .../testdata/contract_test_data_template.json | 0 .../data_catalog_test_data_template.json | 0 .../directory_test_data_template.json | 0 .../testdata/fetch_and_update_datasets.py | 12 +- .../file_storage_test_data_template.json | 0 .../testdata/file_test_data_template.json | 0 .../tests/testdata/generate_test_data.py | 1 + src/metax_api/tests/testdata/request_test.py | 0 src/metax_api/tests/testdata/test_data.json | 0 src/metax_api/tests/utils.py | 378 +- src/metax_api/urls.py | 5 + src/metax_api/utils/__init__.py | 0 src/metax_api/utils/reference_data_loader.py | 14 +- src/metax_api/utils/utils.py | 14 +- src/metax_api/views/__init__.py | 0 src/metax_api/views/router.py | 1 - src/metax_api/views/secure/__init__.py | 0 src/metax_api/views/secure/secure_view.py | 5 +- swagger/README.md | 0 swagger/swagger-yaml-to-html.py | 4 +- swagger/v1/swagger.yaml | 26 +- swagger/v2/swagger.yaml | 24 + 379 files changed, 14127 insertions(+), 2063 deletions(-) mode change 100644 => 100755 .flake8 mode change 100644 => 100755 .github/dependabot.yml mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .travis.yml mode change 100644 => 100755 AUTHORS create mode 100755 Dockerfile create mode 100755 ENV_VARS.md mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README.md create mode 100755 containers/portainer/templates.json mode change 100644 => 100755 deploy-key.enc create mode 100755 docker-compose.yml mode change 100644 => 100755 docs/README.md mode change 100644 => 100755 docs/v1/source/_static/custom.css mode change 100644 => 100755 docs/v1/source/conf.py mode change 100644 => 100755 docs/v1/source/datasets.rst mode change 100644 => 100755 docs/v1/source/end_users.rst mode change 100644 => 100755 docs/v1/source/files.rst mode change 100644 => 100755 docs/v1/source/general.rst mode change 100644 => 100755 docs/v1/source/index.rst mode change 100644 => 100755 docs/v1/source/list_operations.rst mode change 100644 => 100755 docs/v1/source/metax_api.rst mode change 100644 => 100755 docs/v1/source/metax_high_level.png mode change 100644 => 100755 docs/v1/source/metax_high_level.xml mode change 100644 => 100755 docs/v1/source/oai_pmh.rst mode change 100644 => 100755 docs/v1/source/quick_start.rst mode change 100644 => 100755 docs/v1/source/reference_data.rst mode change 100644 => 100755 docs/v2/source/_static/custom.css mode change 100644 => 100755 docs/v2/source/conf.py mode change 100644 => 100755 docs/v2/source/datasets.rst mode change 100644 => 100755 docs/v2/source/end_users.rst mode change 100644 => 100755 docs/v2/source/files.rst mode change 100644 => 100755 docs/v2/source/general.rst mode change 100644 => 100755 docs/v2/source/index.rst mode change 100644 => 100755 docs/v2/source/list_operations.rst mode change 100644 => 100755 docs/v2/source/metax_api.rst mode change 100644 => 100755 docs/v2/source/metax_high_level.png mode change 100644 => 100755 docs/v2/source/metax_high_level.xml mode change 100644 => 100755 docs/v2/source/oai_pmh.rst mode change 100644 => 100755 docs/v2/source/quick_start.rst mode change 100644 => 100755 docs/v2/source/reference_data.rst mode change 100644 => 100755 requirements.in mode change 100644 => 100755 resources/sql/init_test.sql mode change 100644 => 100755 src/.coveragerc mode change 100644 => 100755 src/metax_api/__init__.py mode change 100644 => 100755 src/metax_api/api/__init__.py mode change 100644 => 100755 src/metax_api/api/oaipmh/base/metax_oai_server.py mode change 100644 => 100755 src/metax_api/api/oaipmh/base/view.py mode change 100644 => 100755 src/metax_api/api/rest/base/__init__.py mode change 100644 => 100755 src/metax_api/api/rest/base/api_schemas/catalogrecord.json mode change 100644 => 100755 src/metax_api/api/rest/base/api_schemas/datacatalog.json mode change 100644 => 100755 src/metax_api/api/rest/base/api_schemas/file.json mode change 100644 => 100755 src/metax_api/api/rest/base/schemas/__init__.py mode change 100644 => 100755 src/metax_api/api/rest/base/schemas/att_dataset_schema.json mode change 100644 => 100755 src/metax_api/api/rest/base/schemas/contract_schema.json mode change 100644 => 100755 src/metax_api/api/rest/base/schemas/datacatalog_schema.json mode change 100644 => 100755 src/metax_api/api/rest/base/schemas/datacite_4.1_schema.json mode change 100644 => 100755 src/metax_api/api/rest/base/schemas/file_schema.json mode change 100644 => 100755 src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json mode change 100644 => 100755 src/metax_api/api/rest/base/schemas/ida_dataset_schema.json mode change 100644 => 100755 src/metax_api/api/rest/base/serializers/__init__.py mode change 100644 => 100755 src/metax_api/api/rest/base/serializers/catalog_record_serializer.py mode change 100644 => 100755 src/metax_api/api/rest/base/serializers/common_serializer.py mode change 100644 => 100755 src/metax_api/api/rest/base/serializers/contract_serializer.py mode change 100644 => 100755 src/metax_api/api/rest/base/serializers/data_catalog_serializer.py mode change 100644 => 100755 src/metax_api/api/rest/base/serializers/directory_serializer.py mode change 100644 => 100755 src/metax_api/api/rest/base/serializers/file_serializer.py mode change 100644 => 100755 src/metax_api/api/rest/base/serializers/file_storage_serializer.py mode change 100644 => 100755 src/metax_api/api/rest/base/serializers/serializer_utils.py mode change 100644 => 100755 src/metax_api/api/rest/base/serializers/xml_metadata_serializer.py mode change 100644 => 100755 src/metax_api/api/rest/base/views/__init__.py mode change 100644 => 100755 src/metax_api/api/rest/base/views/api_error_view.py mode change 100644 => 100755 src/metax_api/api/rest/base/views/common_view.py mode change 100644 => 100755 src/metax_api/api/rest/base/views/contract_view.py mode change 100644 => 100755 src/metax_api/api/rest/base/views/data_catalog_view.py mode change 100644 => 100755 src/metax_api/api/rest/base/views/dataset_view.py mode change 100644 => 100755 src/metax_api/api/rest/base/views/directory_view.py mode change 100644 => 100755 src/metax_api/api/rest/base/views/file_storage_view.py mode change 100644 => 100755 src/metax_api/api/rest/base/views/file_view.py mode change 100644 => 100755 src/metax_api/api/rest/base/views/schema_view.py mode change 100644 => 100755 src/metax_api/api/rest/base/xslt/datacite.xslt mode change 100644 => 100755 src/metax_api/api/rest/v2/__init__.py mode change 100644 => 100755 src/metax_api/api/rest/v2/api_schemas/catalogrecord.json mode change 100644 => 100755 src/metax_api/api/rest/v2/api_schemas/datacatalog.json mode change 100644 => 100755 src/metax_api/api/rest/v2/api_schemas/dataset_files_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/api_schemas/file.json mode change 100644 => 100755 src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/schemas/__init__.py mode change 100644 => 100755 src/metax_api/api/rest/v2/schemas/att_dataset_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/schemas/contract_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/schemas/datacatalog_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/schemas/datacite_4.1_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/schemas/dataset_files_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/schemas/dft_dataset_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/schemas/file_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/schemas/harvester_dataset_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/schemas/ida_dataset_schema.json mode change 100644 => 100755 src/metax_api/api/rest/v2/serializers/__init__.py mode change 100644 => 100755 src/metax_api/api/rest/v2/serializers/catalog_record_serializer.py mode change 100644 => 100755 src/metax_api/api/rest/v2/serializers/data_catalog_serializer.py mode change 100644 => 100755 src/metax_api/api/rest/v2/views/__init__.py mode change 100644 => 100755 src/metax_api/api/rest/v2/views/data_catalog_view.py mode change 100644 => 100755 src/metax_api/api/rest/v2/views/dataset_view.py mode change 100644 => 100755 src/metax_api/api/rpc/base/__init__.py mode change 100644 => 100755 src/metax_api/api/rpc/base/views/__init__.py mode change 100644 => 100755 src/metax_api/api/rpc/base/views/common_rpc.py mode change 100644 => 100755 src/metax_api/api/rpc/base/views/dataset_rpc.py mode change 100644 => 100755 src/metax_api/api/rpc/base/views/elasticsearch_rpc.py mode change 100644 => 100755 src/metax_api/api/rpc/base/views/file_rpc.py mode change 100644 => 100755 src/metax_api/api/rpc/base/views/statistic_rpc.py mode change 100644 => 100755 src/metax_api/api/rpc/v2/__init__.py mode change 100644 => 100755 src/metax_api/api/rpc/v2/views/__init__.py mode change 100644 => 100755 src/metax_api/api/rpc/v2/views/dataset_rpc.py create mode 100755 src/metax_api/checks.py mode change 100644 => 100755 src/metax_api/exampledata/dataset_minimal.json mode change 100644 => 100755 src/metax_api/exceptions/__init__.py mode change 100644 => 100755 src/metax_api/exceptions/http_exceptions.py mode change 100644 => 100755 src/metax_api/initialdata/datacatalogs.json mode change 100644 => 100755 src/metax_api/initialdata/filestorages.json mode change 100644 => 100755 src/metax_api/management/__init__.py mode change 100644 => 100755 src/metax_api/management/commands/__init__.py create mode 100755 src/metax_api/management/commands/index_refdata.py mode change 100644 => 100755 src/metax_api/management/commands/loadinitialdata.py create mode 100755 src/metax_api/management/commands/reload_refdata_cache.py mode change 100644 => 100755 src/metax_api/management/commands/updatereferencedata.py mode change 100644 => 100755 src/metax_api/middleware/__init__.py mode change 100644 => 100755 src/metax_api/middleware/add_last_modified_header_to_response.py mode change 100644 => 100755 src/metax_api/middleware/identifyapicaller.py mode change 100644 => 100755 src/metax_api/middleware/request_logging.py mode change 100644 => 100755 src/metax_api/middleware/stream_http_response.py mode change 100644 => 100755 src/metax_api/migrations/0001_initial.py mode change 100644 => 100755 src/metax_api/migrations/0002_create_json_indexes.py mode change 100644 => 100755 src/metax_api/migrations/0003_auto_20180830_1534.py mode change 100644 => 100755 src/metax_api/migrations/0004_auto_20180919_1502.py mode change 100644 => 100755 src/metax_api/migrations/0005_auto_20181101_1524.py mode change 100644 => 100755 src/metax_api/migrations/0006_catalogrecord_date_deprecated.py mode change 100644 => 100755 src/metax_api/migrations/0007_auto_20181126_1436.py mode change 100644 => 100755 src/metax_api/migrations/0008_remove_file_replication_path.py mode change 100644 => 100755 src/metax_api/migrations/0009_catalogrecord_preservation_identifier.py mode change 100644 => 100755 src/metax_api/migrations/0010_auto_20190627_1032.py mode change 100644 => 100755 src/metax_api/migrations/0011_catalogrecord_preservation_dataset_version.py mode change 100644 => 100755 src/metax_api/migrations/0012_auto_20190823_1055.py mode change 100644 => 100755 src/metax_api/migrations/0013_catalogrecord__access_granter.py mode change 100644 => 100755 src/metax_api/migrations/0014_catalogrecord_state.py mode change 100644 => 100755 src/metax_api/migrations/0015_auto_20200218_1607.py mode change 100644 => 100755 src/metax_api/migrations/0016_auto_20200310_1341.py mode change 100644 => 100755 src/metax_api/migrations/0017_catalogrecord_rems_identifier.py mode change 100644 => 100755 src/metax_api/migrations/0018_auto_20200330_1101.py mode change 100644 => 100755 src/metax_api/migrations/0019_auto_20200419_2149.py mode change 100644 => 100755 src/metax_api/migrations/0020_auto_20200429_1512.py mode change 100644 => 100755 src/metax_api/migrations/0021_catalogrecord_next_draft.py mode change 100644 => 100755 src/metax_api/migrations/0022_auto_20200528_1758.py mode change 100644 => 100755 src/metax_api/migrations/0023_catalogrecord_api_meta.py mode change 100644 => 100755 src/metax_api/migrations/0024_catalogrecord_use_doi_for_published.py mode change 100644 => 100755 src/metax_api/migrations/0025_auto_20200811_1050.py mode change 100644 => 100755 src/metax_api/migrations/0026_auto_20200917_1615.py mode change 100644 => 100755 src/metax_api/migrations/0027_auto_20201013_1404.py mode change 100644 => 100755 src/metax_api/migrations/0028_auto_20201020_1010.py mode change 100644 => 100755 src/metax_api/migrations/0029_auto_20201028_1602.py mode change 100644 => 100755 src/metax_api/migrations/__init__.py mode change 100644 => 100755 src/metax_api/migrations/datasets/SYKE_csv.csv mode change 100644 => 100755 src/metax_api/models/__init__.py mode change 100644 => 100755 src/metax_api/models/catalog_record.py mode change 100644 => 100755 src/metax_api/models/catalog_record_v2.py mode change 100644 => 100755 src/metax_api/models/common.py mode change 100644 => 100755 src/metax_api/models/contract.py mode change 100644 => 100755 src/metax_api/models/data_catalog.py mode change 100644 => 100755 src/metax_api/models/directory.py mode change 100644 => 100755 src/metax_api/models/file.py mode change 100644 => 100755 src/metax_api/models/file_storage.py mode change 100644 => 100755 src/metax_api/models/metax_user.py mode change 100644 => 100755 src/metax_api/models/xml_metadata.py mode change 100644 => 100755 src/metax_api/onappstart.py mode change 100644 => 100755 src/metax_api/parsers/__init__.py mode change 100644 => 100755 src/metax_api/parsers/parsers.py mode change 100644 => 100755 src/metax_api/permissions/__init__.py mode change 100644 => 100755 src/metax_api/permissions/permissions.py mode change 100644 => 100755 src/metax_api/renderers/__init__.py mode change 100644 => 100755 src/metax_api/renderers/renderers.py mode change 100644 => 100755 src/metax_api/services/__init__.py mode change 100644 => 100755 src/metax_api/services/api_error_service.py mode change 100644 => 100755 src/metax_api/services/auth_service.py mode change 100644 => 100755 src/metax_api/services/callable_service.py mode change 100644 => 100755 src/metax_api/services/catalog_record_service.py mode change 100644 => 100755 src/metax_api/services/catalog_record_service_v2.py mode change 100644 => 100755 src/metax_api/services/common_service.py mode change 100644 => 100755 src/metax_api/services/data_catalog_service.py mode change 100644 => 100755 src/metax_api/services/datacite_service.py mode change 100644 => 100755 src/metax_api/services/file_service.py mode change 100644 => 100755 src/metax_api/services/pagination.py mode change 100644 => 100755 src/metax_api/services/rabbitmq_service.py mode change 100644 => 100755 src/metax_api/services/redis_cache_service.py mode change 100644 => 100755 src/metax_api/services/reference_data_mixin.py mode change 100644 => 100755 src/metax_api/services/rems_service.py mode change 100644 => 100755 src/metax_api/services/schema_service.py mode change 100644 => 100755 src/metax_api/services/statistic_service.py delete mode 100755 src/metax_api/settings.py create mode 100755 src/metax_api/settings/.env.template create mode 100755 src/metax_api/settings/__init__.py create mode 100755 src/metax_api/settings/components/__init__.py create mode 100755 src/metax_api/settings/components/access_control.py create mode 100755 src/metax_api/settings/components/common.py create mode 100755 src/metax_api/settings/components/elasticsearch.py create mode 100755 src/metax_api/settings/components/externals.py create mode 100755 src/metax_api/settings/components/logging.py create mode 100755 src/metax_api/settings/components/rabbitmq.py create mode 100755 src/metax_api/settings/components/redis.py create mode 100644 src/metax_api/settings/components/rems.py rename resources/data/test_data.json => src/metax_api/settings/environments/__init__.py (100%) mode change 100644 => 100755 create mode 100755 src/metax_api/settings/environments/local.py create mode 100644 src/metax_api/settings/environments/remote.py create mode 100755 src/metax_api/settings/environments/test.py create mode 100755 src/metax_api/settings/environments/travis.py create mode 100755 src/metax_api/tasks/__init__.py create mode 100755 src/metax_api/tasks/refdata/__init__.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/__init__.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/domain/__init__.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/domain/indexable_data.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/domain/organization_data.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/domain/reference_data.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/es_index_data.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/logconf.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/organization_csv_parser.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/requirements.txt create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/es-config/organization_data_index.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/es-config/reference_data_index.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/access_type.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/contributor_role.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/contributor_type.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/event_outcome.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_format_version.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_type.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/funder_type.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/identifier_type.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/license.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/lifecycle_event.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/preservation_event.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/relation_type.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/research_infra.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/resource_type.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/restriction_grounds.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/use_category.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/resources/uri_to_wkt.json create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/service/__init__.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/service/elasticsearch_service.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/service/finto_data_service.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/service/infra_data_service.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/service/local_data_service.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/service/mime_data_service.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/service/organization_service.py create mode 100755 src/metax_api/tasks/refdata/refdata_indexer/service/service_utils.py mode change 100644 => 100755 src/metax_api/templates/secure/auth_success.html mode change 100644 => 100755 src/metax_api/tests/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/oaipmh/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/oaipmh/minimal_api.py mode change 100644 => 100755 src/metax_api/tests/api/oaipmh/syke.py mode change 100644 => 100755 src/metax_api/tests/api/rest/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/serializers/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/serializers/file_serializer.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/serializers/serializer_utils.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/apierrors/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/apierrors/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/common/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/common/auth.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/common/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/common/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/contracts/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/contracts/contracts.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/datacatalogs/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/datacatalogs/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/datacatalogs/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/datasets/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/datasets/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/datasets/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/directories/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/directories/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/directories/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/files/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/files/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/files/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/filestorages/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/filestorages/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/filestorages/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/schemas/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/base/views/schemas/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/apierrors/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/apierrors/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/common/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/common/auth.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/common/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/common/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/contracts/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/contracts/contracts.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datacatalogs/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datacatalogs/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datacatalogs/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datasets/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datasets/api_version_lock.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datasets/drafts.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datasets/filehandling.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datasets/pas.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datasets/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datasets/referencedata.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datasets/rems.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/datasets/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/directories/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/directories/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/directories/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/files/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/files/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/files/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/filestorages/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/filestorages/read.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/filestorages/write.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/schemas/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rest/v2/views/schemas/read.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/base/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/base/views/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/base/views/common_rpc.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/base/views/dataset_rpc.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/base/views/file_rpc.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/base/views/statistic_rpc.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/v2/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/v2/views/__init__.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/v2/views/common_rpc.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/v2/views/file_rpc.py mode change 100644 => 100755 src/metax_api/tests/api/rpc/v2/views/statistic_rpc.py mode change 100644 => 100755 src/metax_api/tests/management/__init__.py mode change 100644 => 100755 src/metax_api/tests/management/commands/__init__.py mode change 100644 => 100755 src/metax_api/tests/management/commands/loadinitialdata.py mode change 100644 => 100755 src/metax_api/tests/middleware/__init__.py mode change 100644 => 100755 src/metax_api/tests/middleware/test_middleware.py mode change 100644 => 100755 src/metax_api/tests/middleware/test_middleware_v2.py mode change 100644 => 100755 src/metax_api/tests/models/__init__.py mode change 100644 => 100755 src/metax_api/tests/models/catalog_record.py mode change 100644 => 100755 src/metax_api/tests/models/common.py mode change 100644 => 100755 src/metax_api/tests/models/data_catalog.py mode change 100644 => 100755 src/metax_api/tests/models/directory.py mode change 100644 => 100755 src/metax_api/tests/models/file.py mode change 100644 => 100755 src/metax_api/tests/rabbitmq/consume.py mode change 100644 => 100755 src/metax_api/tests/services/__init__.py mode change 100644 => 100755 src/metax_api/tests/services/reference_data_mixin.py mode change 100644 => 100755 src/metax_api/tests/testdata/README.md mode change 100644 => 100755 src/metax_api/tests/testdata/__init__.py mode change 100644 => 100755 src/metax_api/tests/testdata/catalog_record_test_data_template.json mode change 100644 => 100755 src/metax_api/tests/testdata/catalog_record_test_data_template_full_att.json mode change 100644 => 100755 src/metax_api/tests/testdata/catalog_record_test_data_template_full_ida.json mode change 100644 => 100755 src/metax_api/tests/testdata/contract_test_data_template.json mode change 100644 => 100755 src/metax_api/tests/testdata/data_catalog_test_data_template.json mode change 100644 => 100755 src/metax_api/tests/testdata/directory_test_data_template.json mode change 100644 => 100755 src/metax_api/tests/testdata/fetch_and_update_datasets.py mode change 100644 => 100755 src/metax_api/tests/testdata/file_storage_test_data_template.json mode change 100644 => 100755 src/metax_api/tests/testdata/file_test_data_template.json mode change 100644 => 100755 src/metax_api/tests/testdata/generate_test_data.py mode change 100644 => 100755 src/metax_api/tests/testdata/request_test.py mode change 100644 => 100755 src/metax_api/tests/testdata/test_data.json mode change 100644 => 100755 src/metax_api/tests/utils.py mode change 100644 => 100755 src/metax_api/utils/__init__.py mode change 100644 => 100755 src/metax_api/utils/reference_data_loader.py mode change 100644 => 100755 src/metax_api/utils/utils.py mode change 100644 => 100755 src/metax_api/views/__init__.py mode change 100644 => 100755 src/metax_api/views/secure/__init__.py mode change 100644 => 100755 src/metax_api/views/secure/secure_view.py mode change 100644 => 100755 swagger/README.md mode change 100644 => 100755 swagger/swagger-yaml-to-html.py mode change 100644 => 100755 swagger/v1/swagger.yaml mode change 100644 => 100755 swagger/v2/swagger.yaml diff --git a/.flake8 b/.flake8 old mode 100644 new mode 100755 diff --git a/.github/dependabot.yml b/.github/dependabot.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 470d6ad2..773f9853 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,8 @@ ubuntu-xenial-16.04-cloudimg-console.log .ropeproject/ .venv_metax/ +.env +.env.* +!.env.template +*.pem +/src/log diff --git a/.travis.yml b/.travis.yml old mode 100644 new mode 100755 index 2ef3eb9f..457c9c43 --- a/.travis.yml +++ b/.travis.yml @@ -3,39 +3,63 @@ dist: trusty language: python python: 3.6 -branches: - only: - - test - - stable - cache: pip addons: + apt: + packages: + - xqilla + - libxqilla-dev postgresql: '9.6' services: -- postgresql + - postgresql + - redis-server -before_install: -- openssl aes-256-cbc -K $encrypted_62ed3fb8af4c_key -iv $encrypted_62ed3fb8af4c_iv -in deploy-key.enc -out deploy-key -d -- rm deploy-key.enc -- chmod 600 deploy-key -- mv deploy-key ~/.ssh/id_rsa -- eval "$(ssh-agent -s)" -- ssh-add ~/.ssh/id_rsa -- sudo apt-get install xqilla libxqilla-dev +env: + global: + - AUTH_SERVER_LOGOUT_URL=http://logout + - DATACITE_PREFIX=10.12345 + - DATACITE_URL=http://mockurl.com + - DATACITE_ETSIN_URL_TEMPLATE=http://mockurl.com/%s + - DATACITE_PASSWORD=not_set + - DATACITE_USERNAME=not_set + - DEBUG=false + - DJANGO_ENV=travis + - DJANGO_SECRET_KEY=lxpRfm8.JOUa7K.eOxZGtL*o+.zt.ybj + - DRAFT_ENABLED=true + - ELASTIC_SEARCH_HOSTS=https://metax.fd-test.csc.fi/es + - ELASTIC_SEARCH_USE_SSL=true + - METAX_DATABASE=metax_db_test + - METAX_DATABASE_PASSWORD= + - METAX_DATABASE_USER=metax_test + - OAI_BASE_URL=http://mockurl.com + - OAI_ETSIN_URL_TEMPLATE=http://mockurl.com/%s + - OAI_ADMIN_EMAIL=mocked@mail.com + - REMS_ENABLED=true + - REMS_API_KEY=not_set + - REMS_BASE_URL=http://mockurl.com + - REMS_ETSIN_URL_TEMPLATE=http://mockurl.com/%s + - REMS_METAX_USER=not_set + - REMS_REPORTER_USER=not_set + - REMS_AUTO_APPROVER=not_set + - REMS_FORM_ID=99 + - ENABLE_V1_ENDPOINTS=true + - ENABLE_V2_ENDPOINTS=true install: - pip install -r requirements.txt -- sudo service postgresql restart before_script: - psql -U postgres < resources/sql/init_test.sql -- sudo mkdir /var/log/metax-api -- sudo chown -R $USER:$USER /var/log/metax-api +- sudo mkdir -p src/log +- sudo chown -R $USER:$USER src/log script: -- cd src && flake8 . && python manage.py migrate metax_api && coverage run --source="." manage.py test metax_api && COVERALLS_REPO_TOKEN=$coveralls_token coveralls && $TRAVIS_BUILD_DIR/.travis-deploy.sh +- cd src && flake8 . +- python manage.py migrate metax_api +- coverage run --source="." manage.py test metax_api +- COVERALLS_REPO_TOKEN=$coveralls_token coveralls notifications: flowdock: diff --git a/AUTHORS b/AUTHORS old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 00000000..77fedea1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM python:3.6 + +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 + +WORKDIR /code + +COPY requirements.txt /code/ + +RUN apt-get update && apt install xqilla libxerces-c-dev build-essential libssl-dev libffi-dev python-dev libxqilla-dev -y + +RUN pip install --upgrade pip wheel +RUN pip install -r requirements.txt + +EXPOSE 8008 +EXPOSE 8006 + +ARG METAX_DATABASE_HOST +ARG REDIS_HOST +ARG RABBITMQ_HOST +ARG RABBIT_MQ_PASSWORD=guest +ARG RABBIT_MQ_USER=guest +ARG ELASTIC_SEARCH_HOST + +ENV METAX_DATABASE_HOST $METAX_DATABASE_HOST +ENV REDIS_HOST $REDIS_HOST +ENV RABBIT_MQ_HOSTS $RABBITMQ_HOST +ENV RABBIT_MQ_PASSWORD $RABBIT_MQ_PASSWORD +ENV RABBIT_MQ_USER $RABBIT_MQ_USER +ENV ELASTIC_SEARCH_HOSTS $ELASTIC_SEARCH_HOST + +# CMD ["python", "/code/manage.py", "runserver", "0.0.0.0:8008"] +CMD ["python", "manage.py", "runsslserver", "--certificate", ".certs/cert.pem","--key", ".certs/key.pem", "0.0.0.0:8008"] \ No newline at end of file diff --git a/ENV_VARS.md b/ENV_VARS.md new file mode 100755 index 00000000..2b4a4a2e --- /dev/null +++ b/ENV_VARS.md @@ -0,0 +1,62 @@ +# Required environmental variables + +copy .env.template to .env and fill the required values from below table. Required column tells if you have to have the variable in the .env file + +| Name | Required | Default | Description | +| ------------------------------ | -------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| ADDITIONAL_USER_PROJECTS_PATH | unknown | "" | No clue if this is important | +| ALLOWED_HOSTS | no | | defines which IP-addresses are allowed to access metax, DJANGO_ENV=local overrides this | +| AUTH_SERVER_LOGOUT_URL | unknown | | Requires testing if this is needed | +| DATACITE_ETSIN_URL_TEMPLATE | yes | | +| DATACITE_PASSWORD | yes | | +| DATACITE_PREFIX | yes | | +| DATACITE_URL | yes | | +| DATACITE_USERNAME | yes | | +| DEBUG | no | False | +| DJANGO_ENV | no | local | Specifies the environment, corresponds with the environments found in src/metax_api/settings/environments/ | +| DJANGO_SECRET_KEY | yes | | +| DRAFT_ENABLED | no | | +| ELASTIC_SEARCH_HOSTS | yes | | Elastic Search instance IP and port | +| ELASTIC_SEARCH_PORT | no | 9200 | Is not used currently, but should be in the future | +| ELASTIC_SEARCH_USE_SSL | yes | | Should Elastic Search queries use https | +| ERROR_FILES_PATH | no | src/log/errors | Error file folder | +| ES_CONFIG_DIR | no | src/metax_api/tasks/refdata/refdata_indexer/resources/es-config | metax-ops compatibility | +| LOCAL_REF_DATA_FOLDER | no | src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata | metax-ops compatibility | +| LOGGING_DEBUG_HANDLER_FILE | no | src/log/metax_api.log | metax-ops compatibility | +| LOGGING_GENERAL_HANDLER_FILE | no | src/log/metax_api.log | metax-ops compatibility | +| LOGGING_JSON_FILE_HANDLER_FILE | no | src/log/metax_api.json.log | metax-ops compatibility | +| METAX_API_ROOT | yes | | Metax internal variable, must be https | +| METAX_DATABASE | yes | | Postgres database name | +| METAX_DATABASE_PASSWORD | yes | | Postgres database password | +| METAX_DATABASE_PORT | no | 5432 | Postgres instance exposed port | +| METAX_DATABASE_USER | yes | | Postgres user which owns the database | +| METAX_ENV | no | local_development | Used by Metax internally, should be replaced with DJANGO_ENV in the future | +| OAI_BASE_URL | yes | | +| ORG_FILE_PATH | yes | src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv | metax-ops compatibility | +| RABBIT_MQ_HOSTS | yes | | RabbitMQ instance IP | +| RABBIT_MQ_PASSWORD | no | guest | +| RABBIT_MQ_PORT | no | 5672 | +| RABBIT_MQ_TTV_ENABLED | no | | +| RABBIT_MQ_USER | no | guest | +| RABBIT_MQ_VHOST | no | | +| RABBIT_MQ_VHOST_TTV | no | | +| REDIS_HOST | yes | | Redis instance IP | +| REDIS_LOCALHOST_PORT | unknown | 6379 | Not sure if all references to this are gone | +| REDIS_PASSWORD | no | | +| REDIS_PORT | no | 6379 | +| REDIS_TEST_DB | yes | | Pick a number, any number | +| REDIS_USE_PASSWORD | no | false | +| REMS_API_KEY | no | +| REMS_AUTO_APPROVER | no | +| REMS_BASE_URL | no | +| REMS_ENABLED | no | +| REMS_ETSIN_URL_TEMPLATE | yes | | +| REMS_FORM_ID | yes | | +| REMS_METAX_USER | no | +| REMS_REPORTER_USER | no | +| SERVER_DOMAIN_NAME | no | +| TRAVIS | no | false | Metax internal used to figure out if Metax is running on Travis virtual machine | +| V1_ENABLED | no | | +| V2_ENABLED | no | | +| VALIDATE_TOKEN_URL | yes | | +| WKT_FILENAME | no | src/metax_api/tasks/refdata/refdata_indexer/resources/uri_to_wkt.json | \ No newline at end of file diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 56110a73..03a0516b --- a/README.md +++ b/README.md @@ -13,3 +13,126 @@ License Copyright (c) 2018-2020 Ministry of Education and Culture, Finland Licensed under [GNU GPLv2 License](LICENSE) + + +## Setting up local development environment + +### Prerequisites + +#### Docker-Engine + +Install Docker-Engine either following instructions below or looking up your platform specific instructions [from docs.docker.com][1] + +##### Linux + +`$ curl -fsSL https://get.docker.com -o get-docker.sh` + +`$ sudo sh get-docker.sh` + +`$ sudo usermod -aG docker $USER` + +Log out and back in to activate non-sudo docker capabilities + +##### Mac + +https://docs.docker.com/docker-for-mac/install/ + +#### Portainer (Optional) + +We will use portainer container management tool for monitoring various development dependencies. Command below will start portainer on every system startup. + +`$ docker volume create portainer_data` (optional for mac) + +`$ docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce` + +Finish the Portainer setup by logging in at http://localhost:9000, create a local endpoint from the Portainer interface. + +#### Docker commands + +__NOTICE If you want to start the services everytime your computer boots, replace `--restart=unless-stopped` with `--restart=always`__ + +Run the following docker commands to start services: + +##### Redis + +`docker run -d -p 6379:6379 --name metax-redis -v metax-redis:/data --restart=unless-stopped redis` + +##### Postgres + +`docker run -d -p 5432:5432 --name metax-postgres -v metax-postgres:/var/lib/postgresql96/data -e POSTGRES_USER=metax_db_user -e POSTGRES_PASSWORD=YMDLekQMqrVKcs3 -e POSTGRES_DB=metax_db --restart=unless-stopped postgres:9` + +__NOTICE: copy values of `POSTGRES_USER`, `POSTGRES_PASSWORD` and `POSTGRES_DB` into your `.env` files as `METAX_DATABASE_USER`, `METAX_DATABASE_PASSWORD` and `METAX_DATABASE`__ + +##### Elasticsearch + +`docker run -d -p 9200:9200 -p 9300:9300 -v metax-es:/usr/share/elasticsearch/data --name metax-es -e discovery.type=single-node --restart=unless-stopped elasticsearch:7.9.2` + +##### RabbitMQ + +`docker run -d -p 5671:5671 -p 5672:5672 -p 15672:15672 -v metax-rabbitmq:/var/lib/rabbitmq --name metax-rabbitmq --restart=unless-stopped rabbitmq:3-management` + +#### mkcerts + +Install [mkcerts][2] and run `mkcert -install` and after it the following command: +`mkcert -cert-file cert.pem -key-file key.pem 0.0.0.0 localhost 127.0.0.1 ::1 metax.csc.local 20.20.20.20` +Move the `cert.pem` and `key.pem` to `src/.certs` folder (create the folder if not present). + +### Required environmental variables + +copy `src/metax_api/settings/.env.template` as `src/metax_api/settings/.env` and fill required variables, you can find examples in ENV_VARS.md + +### Create log directory + +`mkdir -p src/log/errors` + +### Run Metax inside a container (Optional) + +Check the IP addresses of Redis, RabbitMQ, ElasticSearch and Postgres:9 either from Portainer container list (click the link in the container name to see all attributes) or by going to portainer network tab or by typing `docker container ps` followed by `docker network inspect bridge` + +Build new docker image from repository root with this command (change ip-addresses to real ones: + +`docker build -t metax-api:latest --build-arg METAX_DATABASE_HOST=xxx.xx.x.x --build-arg REDIS_HOST=xxx.xx.x.x --build-arg RABBITMQ_HOST=xxx.xx.x.x --build-arg ELASTIC_SEARCH_HOST=xxx.xx.x.x:xxxx .` + +Run the built container with command: + +`docker run -it --name metax-web --mount type=bind,source="$(pwd)"/src,target=/code -p 8008:8008 --rm metax-api:latest` + +You should see metax-server starting at port 8008 with hot reload enabled + +### Initial setup commands +docker network inspect bridge +IF you configured metax-container, access the command line of the container with `docker exec -it metax-web bash` + +__NOTICE: Skip activating virtualenv and navigating to src folder if you have metax running on container__ + +Activate your python 3.6 virtualenv, `cd` into `src` folder and run following commands: + +setup the database with migrate command: + +`python manage.py migrate` + +__NOTICE: Skip following steps if your running metax on container and have terminal open in the container__ + +start the development server with: +`python manage.py runsslserver --certificate .certs/cert.pem --key .certs/key.pem 8008` + +Open another terminal and `cd` into `src`, and load the initial data with following commands: + +__These commands must be run in both setups (container/not-container metax)__ + +`python manage.py index_refdata` + +`python manage.py reload_refdata_cache` + +`python manage.py loadinitialdata` + +`python manage.py loaddata metax_api/tests/testdata/test_data.json` + +run the tests with command `DJANGO_ENV=test python manage.py test --failfast --keepdb -v 0` + +Metax api is available from your browser at https://localhost:8008 + + +[1]: https://docs.docker.com/engine/install/ +[2]: https://github.com/FiloSottile/mkcert + diff --git a/containers/portainer/templates.json b/containers/portainer/templates.json new file mode 100755 index 00000000..7d2d1f33 --- /dev/null +++ b/containers/portainer/templates.json @@ -0,0 +1,234 @@ +{ + "version": "2", + "templates": [ + { + "type": 1, + "title": "Nginx", + "description": "High performance web server", + "categories": [ + "webserver" + ], + "platform": "linux", + "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/nginx.png", + "image": "nginx:latest", + "ports": [ + "80/tcp", + "443/tcp" + ], + "volumes": [ + { + "container": "/etc/nginx" + }, + { + "container": "/usr/share/nginx/html" + } + ] + }, + { + "type": 1, + "title": "PostgreSQL", + "description": "The most advanced open-source database", + "categories": [ + "database" + ], + "platform": "linux", + "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/postgres.png", + "image": "postgres:latest", + "env": [ + { + "name": "POSTGRES_USER", + "label": "Superuser" + }, + { + "name": "POSTGRES_PASSWORD", + "label": "Superuser password" + } + ], + "ports": [ + "5432/tcp" + ], + "volumes": [ + { + "container": "/var/lib/postgresql/data" + } + ] + }, + { + "type": 1, + "title": "PostgreSQL 9", + "description": "The most advanced open-source database", + "categories": [ + "database" + ], + "platform": "linux", + "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/postgres.png", + "image": "postgres:9", + "env": [ + { + "name": "POSTGRES_USER", + "label": "postgres user", + "default": "metax_db_user" + }, + { + "name": "POSTGRES_PASSWORD", + "label": "postgres password", + "default": "YMDLekQMqrVKcs3" + } + ], + "ports": [ + "5432/tcp" + ], + "volumes": [ + { + "container": "/var/lib/postgresql96/data" + } + ] + }, + { + "type": 1, + "title": "Elasticsearch", + "description": "Open-source search and analytics engine", + "categories": [ + "database" + ], + "platform": "linux", + "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/elasticsearch.png", + "image": "elasticsearch:7.9.2", + "ports": [ + "9200/tcp", + "9300/tcp" + ], + "volumes": [ + { + "container": "/usr/share/elasticsearch/data" + } + ], + "env": [ + { + "name": "discovery.type", + "label": "Discovery Type", + "default": "single-node" + } + ] + }, + { + "type": 1, + "title": "Redis", + "description": "Open-source in-memory data structure store", + "categories": [ + "database" + ], + "platform": "linux", + "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/redis.png", + "image": "redis:latest", + "ports": [ + "6379/tcp" + ], + "volumes": [ + { + "container": "/data" + } + ] + }, + { + "type": 1, + "title": "Bitnami-Redis", + "description": "Open-source in-memory data structure store", + "categories": [ + "database" + ], + "platform": "linux", + "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/redis.png", + "image": "bitnami/redis:latest", + "ports": [ + "6379/tcp" + ], + "volumes": [ + { + "container": "/data" + } + ], + "env": [ + { + "name": "ALLOW_EMPTY_PASSWORD", + "label": "Allow empty password", + "default": "no" + }, + { + "name": "REDIS_PASSWORD", + "label": "Redis password", + "default": "NDE6oXov3hpoZV" + } + ] + }, + { + "type": 1, + "title": "Redis Sentinel by Bitnami ", + "description": "Open-source in-memory data structure store", + "categories": [ + "database" + ], + "platform": "linux", + "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/redis.png", + "image": "bitnami/redis-sentinel:latest", + "ports": [ + "26379/tcp" + ], + "volumes": [ + { + "container": "/data" + } + ], + "env": [ + { + "name": "REDIS_MASTER_HOST", + "label": "Host of the Redis master to monitor", + "default": "redis" + }, + { + "name": "REDIS_MASTER_PORT_NUMBER", + "label": "Port of the Redis master to monitor", + "default": "6379" + }, + { + "name": "REDIS_MASTER_SET", + "label": "Name of the set of Redis instances to monitor", + "default": "mymaster" + }, + { + "name": "REDIS_MASTER_PASSWORD", + "label": "Password to authenticate with the master", + "default": "7RewDzhRdXhoYc" + }, + { + "name": "REDIS_MASTER_USER", + "label": "Username to authenticate with when ACL is enabled for the master" + }, + { + "name": "REDIS_SENTINEL_PASSWORD", + "label": "Password to authenticate with this sentinel and to authenticate to other sentinels" + } + ] + }, + { + "type": 1, + "title": "RabbitMQ", + "description": "Highly reliable enterprise messaging system", + "categories": [ + "messaging" + ], + "platform": "linux", + "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/rabbitmq.png", + "image": "rabbitmq:latest", + "ports": [ + "5671/tcp", + "5672/tcp" + ], + "volumes": [ + { + "container": "/var/lib/rabbitmq" + } + ] + } + ] + } \ No newline at end of file diff --git a/deploy-key.enc b/deploy-key.enc old mode 100644 new mode 100755 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 00000000..82b62f9f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +version: "3.8" + +services: + db: + image: library/postgres:9 + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + volumes: + - postgres_data:/var/lib/postgresql/data/ + web: + build: . + command: python manage.py runserver 0.0.0.0:8008 + volumes: + - src:/code + ports: + - "8008:8008" + depends_on: + - db + - redis + # env_file: + # - ./src/metax_api/settings/.env + environment: + - REDIS_HOST=redis + links: + - redis:redis + redis: + image: library/redis + ports: + - "6379:6379" +volumes: + postgres_data: + src: diff --git a/docs/README.md b/docs/README.md old mode 100644 new mode 100755 diff --git a/docs/v1/source/_static/custom.css b/docs/v1/source/_static/custom.css old mode 100644 new mode 100755 diff --git a/docs/v1/source/conf.py b/docs/v1/source/conf.py old mode 100644 new mode 100755 diff --git a/docs/v1/source/datasets.rst b/docs/v1/source/datasets.rst old mode 100644 new mode 100755 diff --git a/docs/v1/source/end_users.rst b/docs/v1/source/end_users.rst old mode 100644 new mode 100755 diff --git a/docs/v1/source/files.rst b/docs/v1/source/files.rst old mode 100644 new mode 100755 diff --git a/docs/v1/source/general.rst b/docs/v1/source/general.rst old mode 100644 new mode 100755 diff --git a/docs/v1/source/index.rst b/docs/v1/source/index.rst old mode 100644 new mode 100755 diff --git a/docs/v1/source/list_operations.rst b/docs/v1/source/list_operations.rst old mode 100644 new mode 100755 diff --git a/docs/v1/source/metax_api.rst b/docs/v1/source/metax_api.rst old mode 100644 new mode 100755 diff --git a/docs/v1/source/metax_high_level.png b/docs/v1/source/metax_high_level.png old mode 100644 new mode 100755 diff --git a/docs/v1/source/metax_high_level.xml b/docs/v1/source/metax_high_level.xml old mode 100644 new mode 100755 diff --git a/docs/v1/source/oai_pmh.rst b/docs/v1/source/oai_pmh.rst old mode 100644 new mode 100755 diff --git a/docs/v1/source/quick_start.rst b/docs/v1/source/quick_start.rst old mode 100644 new mode 100755 diff --git a/docs/v1/source/reference_data.rst b/docs/v1/source/reference_data.rst old mode 100644 new mode 100755 diff --git a/docs/v2/source/_static/custom.css b/docs/v2/source/_static/custom.css old mode 100644 new mode 100755 diff --git a/docs/v2/source/conf.py b/docs/v2/source/conf.py old mode 100644 new mode 100755 diff --git a/docs/v2/source/datasets.rst b/docs/v2/source/datasets.rst old mode 100644 new mode 100755 diff --git a/docs/v2/source/end_users.rst b/docs/v2/source/end_users.rst old mode 100644 new mode 100755 diff --git a/docs/v2/source/files.rst b/docs/v2/source/files.rst old mode 100644 new mode 100755 diff --git a/docs/v2/source/general.rst b/docs/v2/source/general.rst old mode 100644 new mode 100755 diff --git a/docs/v2/source/index.rst b/docs/v2/source/index.rst old mode 100644 new mode 100755 diff --git a/docs/v2/source/list_operations.rst b/docs/v2/source/list_operations.rst old mode 100644 new mode 100755 diff --git a/docs/v2/source/metax_api.rst b/docs/v2/source/metax_api.rst old mode 100644 new mode 100755 diff --git a/docs/v2/source/metax_high_level.png b/docs/v2/source/metax_high_level.png old mode 100644 new mode 100755 diff --git a/docs/v2/source/metax_high_level.xml b/docs/v2/source/metax_high_level.xml old mode 100644 new mode 100755 diff --git a/docs/v2/source/oai_pmh.rst b/docs/v2/source/oai_pmh.rst old mode 100644 new mode 100755 diff --git a/docs/v2/source/quick_start.rst b/docs/v2/source/quick_start.rst old mode 100644 new mode 100755 diff --git a/docs/v2/source/reference_data.rst b/docs/v2/source/reference_data.rst old mode 100644 new mode 100755 diff --git a/requirements.in b/requirements.in old mode 100644 new mode 100755 index 5257d556..4326b1cb --- a/requirements.in +++ b/requirements.in @@ -1,30 +1,40 @@ -coveralls # code coverage reportin in travis -datacite # BSD-license. convert datasets to datacite xml. datacite metadata store api wrappers +coveralls # code coverage reportin in travis +datacite # BSD-license. convert datasets to datacite xml. datacite metadata store api wrappers python-dateutil -django # BSD-license +django # BSD-license elasticsearch>=7.0.0,<8.0.0 -hiredis # Used by redis (redis-py) for parser -djangorestframework # BSD-license -django-rainbowtests # colored test output -flake8 # MIT-license -gevent # gunicorn dep -gunicorn # MIT-license -ipdb # dev tool +hiredis # Used by redis (redis-py) for parser +djangorestframework # BSD-license +django-rainbowtests # colored test output +flake8 # MIT-license +gevent # gunicorn dep +gunicorn # MIT-license +ipdb # dev tool jsonschema lxml pika -psycopg2-binary # LGPL with exceptions or ZPL -PyJWT # MIT-license +psycopg2-binary # LGPL with exceptions or ZPL +PyJWT # MIT-license pyoai python-simplexquery pytz pyyaml redis -requests # Apache 2.0-license -responses # Apache 2.0-license -structlog # Apache 2.0-license, MIT -simplejson # MIT-license +requests # Apache 2.0-license +responses # Apache 2.0-license +structlog # Apache 2.0-license, MIT +simplejson # MIT-license urllib3 xmltodict # MIT-license pip-tools # keeping dependencies updated -traitlets>=4.3.2,<5.0.0 # via ipython, version 5.0+ is Python 3.7+ only \ No newline at end of file +traitlets>=4.3.2,<5.0.0 # via ipython, version 5.0+ is Python 3.7+ only +xmltodict # MIT-license +pip-tools # keeping dependencies updated +django-environ +django-split-settings +icecream +rdflib +django-sslserver +django-watchman + + diff --git a/requirements.txt b/requirements.txt index e9bd2d59..235ab8ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,32 +5,41 @@ # pip-compile # asgiref==3.2.10 # via django +asttokens==2.0.4 # via icecream attrs==20.3.0 # via jsonschema backcall==0.2.0 # via ipython certifi==2020.6.20 # via elasticsearch, requests chardet==3.0.4 # via requests click==7.1.2 # via pip-tools +colorama==0.4.4 # via icecream coverage==5.3 # via coveralls coveralls==2.1.2 # via -r requirements.in datacite==1.0.1 # via -r requirements.in decorator==4.4.2 # via ipython, traitlets +django-environ==0.4.5 # via -r requirements.in django-rainbowtests==0.6.0 # via -r requirements.in -django==3.1.2 # via -r requirements.in, django-rainbowtests, djangorestframework -djangorestframework==3.12.1 # via -r requirements.in +django-split-settings==1.0.1 # via -r requirements.in +django-sslserver==0.22 # via -r requirements.in +django-watchman==1.2.0 # via -r requirements.in +djangorestframework==3.12.2 # via -r requirements.in +django==3.1.4 # via -r requirements.in, django-rainbowtests, django-sslserver, django-watchman, djangorestframework docopt==0.6.2 # via coveralls elasticsearch==7.10.0 # via -r requirements.in +executing==0.5.3 # via icecream flake8==3.8.4 # via -r requirements.in gevent==20.9.0 # via -r requirements.in greenlet==0.4.17 # via gevent gunicorn==20.0.4 # via -r requirements.in hiredis==1.1.0 # via -r requirements.in +icecream==2.0.0 # via -r requirements.in idna==2.10 # via requests ipdb==0.13.4 # via -r requirements.in ipython-genutils==0.2.0 # via traitlets ipython==7.16.1 # via ipdb +isodate==0.6.0 # via rdflib jedi==0.17.2 # via ipython jsonschema==3.2.0 # via -r requirements.in, datacite -lxml==4.6.1 # via -r requirements.in, datacite, pyoai +lxml==4.6.2 # via -r requirements.in, datacite, pyoai mccabe==0.6.1 # via flake8 parso==0.7.1 # via jedi pexpect==4.8.0 # via ipython @@ -42,19 +51,21 @@ psycopg2-binary==2.8.6 # via -r requirements.in ptyprocess==0.6.0 # via pexpect pycodestyle==2.6.0 # via flake8 pyflakes==2.2.0 # via flake8 -pygments==2.7.2 # via ipython +pygments==2.7.2 # via icecream, ipython pyjwt==1.7.1 # via -r requirements.in pyoai==2.5.0 # via -r requirements.in +pyparsing==2.4.7 # via rdflib pyrsistent==0.17.3 # via jsonschema python-dateutil==2.8.1 # via -r requirements.in python-simplexquery==1.0.5.3 # via -r requirements.in -pytz==2020.1 # via -r requirements.in, django +pytz==2020.5 # via -r requirements.in, django pyyaml==5.3.1 # via -r requirements.in +rdflib==5.0.0 # via -r requirements.in redis==3.5.3 # via -r requirements.in requests==2.25.0 # via -r requirements.in, coveralls, datacite, responses responses==0.12.1 # via -r requirements.in simplejson==3.17.2 # via -r requirements.in -six==1.15.0 # via jsonschema, pip-tools, pyoai, python-dateutil, responses, structlog, traitlets +six==1.15.0 # via asttokens, isodate, jsonschema, pip-tools, pyoai, python-dateutil, rdflib, responses, structlog, traitlets sqlparse==0.4.1 # via django structlog==20.1.0 # via -r requirements.in traitlets==4.3.3 # via -r requirements.in, ipython diff --git a/resources/sql/init_test.sql b/resources/sql/init_test.sql old mode 100644 new mode 100755 diff --git a/src/.coveragerc b/src/.coveragerc old mode 100644 new mode 100755 diff --git a/src/metax_api/__init__.py b/src/metax_api/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/__init__.py b/src/metax_api/api/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/oaipmh/base/metax_oai_server.py b/src/metax_api/api/oaipmh/base/metax_oai_server.py old mode 100644 new mode 100755 index a4f99ecf..1d602bbd --- a/src/metax_api/api/oaipmh/base/metax_oai_server.py +++ b/src/metax_api/api/oaipmh/base/metax_oai_server.py @@ -7,8 +7,8 @@ import datetime -from django.utils import timezone from django.conf import settings +from django.utils import timezone from oaipmh import common from oaipmh.common import ResumptionOAIPMH from oaipmh.error import IdDoesNotExistError, BadArgumentError, NoRecordsMatchError, CannotDisseminateFormatError diff --git a/src/metax_api/api/oaipmh/base/view.py b/src/metax_api/api/oaipmh/base/view.py old mode 100644 new mode 100755 index 410c2f71..b4b769e1 --- a/src/metax_api/api/oaipmh/base/view.py +++ b/src/metax_api/api/oaipmh/base/view.py @@ -5,17 +5,16 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import oaipmh.metadata as oaimd +import oaipmh.server as oaiserver from django.conf import settings from django.http import HttpResponse from lxml import etree from lxml.etree import SubElement -import oaipmh.metadata as oaimd -import oaipmh.server as oaiserver from .metax_oai_server import MetaxOAIServer, OAI_DC_MDPREFIX, OAI_DATACITE_MDPREFIX, OAI_FAIRDATA_DATACITE_MDPREFIX, \ OAI_DC_URNRESOLVER_MDPREFIX - NS_OAIDC_DATACITE = 'http://schema.datacite.org/oai/oai-1.0/' NS_OAIPMH = 'http://www.openarchives.org/OAI/2.0/' diff --git a/src/metax_api/api/rest/base/__init__.py b/src/metax_api/api/rest/base/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/api_schemas/catalogrecord.json b/src/metax_api/api/rest/base/api_schemas/catalogrecord.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/api_schemas/datacatalog.json b/src/metax_api/api/rest/base/api_schemas/datacatalog.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/api_schemas/file.json b/src/metax_api/api/rest/base/api_schemas/file.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/schemas/__init__.py b/src/metax_api/api/rest/base/schemas/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/schemas/att_dataset_schema.json b/src/metax_api/api/rest/base/schemas/att_dataset_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/schemas/contract_schema.json b/src/metax_api/api/rest/base/schemas/contract_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/schemas/datacatalog_schema.json b/src/metax_api/api/rest/base/schemas/datacatalog_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/schemas/datacite_4.1_schema.json b/src/metax_api/api/rest/base/schemas/datacite_4.1_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/schemas/file_schema.json b/src/metax_api/api/rest/base/schemas/file_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json b/src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/schemas/ida_dataset_schema.json b/src/metax_api/api/rest/base/schemas/ida_dataset_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/serializers/__init__.py b/src/metax_api/api/rest/base/serializers/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/serializers/catalog_record_serializer.py b/src/metax_api/api/rest/base/serializers/catalog_record_serializer.py old mode 100644 new mode 100755 index 78ae02c7..139849e9 --- a/src/metax_api/api/rest/base/serializers/catalog_record_serializer.py +++ b/src/metax_api/api/rest/base/serializers/catalog_record_serializer.py @@ -24,7 +24,6 @@ from .data_catalog_serializer import DataCatalogSerializer from .serializer_utils import validate_json - _logger = logging.getLogger(__name__) # when end user creates a record, strip all fields except these diff --git a/src/metax_api/api/rest/base/serializers/common_serializer.py b/src/metax_api/api/rest/base/serializers/common_serializer.py old mode 100644 new mode 100755 index a6d7d274..7f9f57fc --- a/src/metax_api/api/rest/base/serializers/common_serializer.py +++ b/src/metax_api/api/rest/base/serializers/common_serializer.py @@ -5,10 +5,10 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from copy import deepcopy +import logging from collections import OrderedDict +from copy import deepcopy from datetime import datetime -import logging from django.db import transaction from django.db.models.query import QuerySet @@ -16,11 +16,10 @@ from rest_framework.relations import PKOnlyObject from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ValidationError -from metax_api.exceptions import Http400 +from metax_api.exceptions import Http400 from metax_api.models import Common - _logger = logging.getLogger(__name__) diff --git a/src/metax_api/api/rest/base/serializers/contract_serializer.py b/src/metax_api/api/rest/base/serializers/contract_serializer.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py b/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/serializers/directory_serializer.py b/src/metax_api/api/rest/base/serializers/directory_serializer.py old mode 100644 new mode 100755 index 30e5f9b7..42ab6813 --- a/src/metax_api/api/rest/base/serializers/directory_serializer.py +++ b/src/metax_api/api/rest/base/serializers/directory_serializer.py @@ -7,8 +7,8 @@ import logging -from rest_framework.serializers import ValidationError from rest_framework import serializers +from rest_framework.serializers import ValidationError from metax_api.models import Directory from .common_serializer import CommonSerializer, LightSerializer diff --git a/src/metax_api/api/rest/base/serializers/file_serializer.py b/src/metax_api/api/rest/base/serializers/file_serializer.py old mode 100644 new mode 100755 index 9588ac62..077c07ff --- a/src/metax_api/api/rest/base/serializers/file_serializer.py +++ b/src/metax_api/api/rest/base/serializers/file_serializer.py @@ -7,10 +7,10 @@ import logging +from django.conf import settings from rest_framework import serializers from rest_framework.serializers import ValidationError from rest_framework.validators import UniqueValidator -from django.conf import settings from metax_api.models import Directory, File, FileStorage from metax_api.services import FileService as FS @@ -19,7 +19,6 @@ from .file_storage_serializer import FileStorageSerializer from .serializer_utils import validate_json - _logger = logging.getLogger(__name__) diff --git a/src/metax_api/api/rest/base/serializers/file_storage_serializer.py b/src/metax_api/api/rest/base/serializers/file_storage_serializer.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/serializers/serializer_utils.py b/src/metax_api/api/rest/base/serializers/serializer_utils.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/serializers/xml_metadata_serializer.py b/src/metax_api/api/rest/base/serializers/xml_metadata_serializer.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/views/__init__.py b/src/metax_api/api/rest/base/views/__init__.py old mode 100644 new mode 100755 index 902ea307..760b837e --- a/src/metax_api/api/rest/base/views/__init__.py +++ b/src/metax_api/api/rest/base/views/__init__.py @@ -11,6 +11,6 @@ from .data_catalog_view import DataCatalogViewSet from .dataset_view import DatasetViewSet from .directory_view import DirectoryViewSet -from .file_view import FileViewSet from .file_storage_view import FileStorageViewSet +from .file_view import FileViewSet from .schema_view import SchemaViewSet diff --git a/src/metax_api/api/rest/base/views/api_error_view.py b/src/metax_api/api/rest/base/views/api_error_view.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/views/common_view.py b/src/metax_api/api/rest/base/views/common_view.py old mode 100644 new mode 100755 index 3e856fc8..001dfd07 --- a/src/metax_api/api/rest/base/views/common_view.py +++ b/src/metax_api/api/rest/base/views/common_view.py @@ -5,8 +5,8 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from os import path import logging +from os import path from django.http import HttpResponse, Http404 from rest_framework import status diff --git a/src/metax_api/api/rest/base/views/contract_view.py b/src/metax_api/api/rest/base/views/contract_view.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/views/data_catalog_view.py b/src/metax_api/api/rest/base/views/data_catalog_view.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/views/dataset_view.py b/src/metax_api/api/rest/base/views/dataset_view.py old mode 100644 new mode 100755 index 8074e12c..e8d75553 --- a/src/metax_api/api/rest/base/views/dataset_view.py +++ b/src/metax_api/api/rest/base/views/dataset_view.py @@ -5,19 +5,24 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from json import dump, load import logging +from json import dump +from django.conf import settings from django.http import Http404 from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response -import yaml from metax_api.exceptions import Http403, Http400 from metax_api.models import CatalogRecord, Common, DataCatalog, File, Directory from metax_api.renderers import XMLRenderer -from metax_api.services import CatalogRecordService, CommonService as CS, RabbitMQService as rabbitmq +from metax_api.services import ( + CatalogRecordService, + CommonService as CS, + RabbitMQService as rabbitmq, +) +from metax_api.settings import env from .common_view import CommonViewSet from ..serializers import CatalogRecordSerializer, LightFileSerializer @@ -35,9 +40,9 @@ class DatasetViewSet(CommonViewSet): serializer_class = CatalogRecordSerializer object = CatalogRecord - select_related = ['data_catalog', 'contract'] + select_related = ["data_catalog", "contract"] - lookup_field = 'pk' + lookup_field = "pk" def __init__(self, *args, **kwargs): # As opposed to other views, do not set json schema here @@ -50,7 +55,9 @@ def get_object(self): try: cr = super(DatasetViewSet, self).get_object() except Http404: - if self.service_class.is_primary_key(self.kwargs.get(self.lookup_field, False)): + if self.service_class.is_primary_key( + self.kwargs.get(self.lookup_field, False) + ): # fail on pk search is clear... raise @@ -63,15 +70,17 @@ def get_object(self): def retrieve(self, request, *args, **kwargs): from metax_api.services.datacite_service import DataciteException + self.queryset_search_params = {} CS.set_if_modified_since_filter(self.request, self.queryset_search_params) res = super(DatasetViewSet, self).retrieve(request, *args, **kwargs) - if 'dataset_format' in request.query_params: + if "dataset_format" in request.query_params: try: res.data = self.service_class.transform_datasets_to_format( - res.data, request.query_params['dataset_format'], request=request) + res.data, request.query_params["dataset_format"], request=request + ) except DataciteException as e: raise Http400(str(e)) request.accepted_renderer = XMLRenderer() @@ -79,25 +88,27 @@ def retrieve(self, request, *args, **kwargs): return res def _retrieve_by_preferred_identifier(self, request, *args, **kwargs): - lookup_value = request.query_params['preferred_identifier'] + lookup_value = request.query_params["preferred_identifier"] self.kwargs[self.lookup_field] = lookup_value self.request.GET._mutable = True - self.request.query_params['no_pagination'] = 'true' - self.request.query_params['pagination'] = 'false' - self.request.GET._mutable = False # hehe + self.request.query_params["no_pagination"] = "true" + self.request.query_params["pagination"] = "false" + self.request.GET._mutable = False # hehe return self.retrieve(request, *args, **kwargs) def list(self, request, *args, **kwargs): # best to specify a variable for parameters intended for filtering purposes in get_queryset(), # because other api's may use query parameters of the same name, which can # mess up filtering if get_queryset() uses request.query_parameters directly. - self.queryset_search_params = self.service_class.get_queryset_search_params(request) + self.queryset_search_params = self.service_class.get_queryset_search_params( + request + ) - if 'preferred_identifier' in request.query_params: + if "preferred_identifier" in request.query_params: return self._retrieve_by_preferred_identifier(request, *args, **kwargs) # actually a nested url /datasets/id/metadata_versions/id. this is probably a very screwed up way to do this... - if 'identifier' in kwargs and 'metadata_version_identifier' in kwargs: + if "identifier" in kwargs and "metadata_version_identifier" in kwargs: return self._metadata_version_get(request, *args, **kwargs) return super(DatasetViewSet, self).list(request, *args, **kwargs) @@ -106,22 +117,26 @@ def _metadata_version_get(self, request, *args, **kwargs): """ Get single research_dataset version. """ - assert 'identifier' in kwargs - assert 'metadata_version_identifier' in kwargs + assert "identifier" in kwargs + assert "metadata_version_identifier" in kwargs # get_object() expects the following... - self.kwargs[self.lookup_field] = kwargs['identifier'] + self.kwargs[self.lookup_field] = kwargs["identifier"] cr = self.get_object() - search_params = { 'catalog_record_id': cr.id } + search_params = {"catalog_record_id": cr.id} - if self.service_class.is_primary_key(kwargs['metadata_version_identifier']): - search_params['id'] = kwargs['metadata_version_identifier'] + if self.service_class.is_primary_key(kwargs["metadata_version_identifier"]): + search_params["id"] = kwargs["metadata_version_identifier"] else: - search_params['metadata_version_identifier'] = kwargs['metadata_version_identifier'] + search_params["metadata_version_identifier"] = kwargs[ + "metadata_version_identifier" + ] try: - research_dataset = cr.research_dataset_versions.get(**search_params).research_dataset + research_dataset = cr.research_dataset_versions.get( + **search_params + ).research_dataset except: raise Http404 @@ -132,12 +147,15 @@ def _metadata_version_get(self, request, *args, **kwargs): # possible to use the serializer, since an older metadata version of a ds # is not stored as part of the cr, but in the table ResearchDatasetVersion. # therefore, perform this checking and stripping separately here. - research_dataset = self.service_class.check_and_remove_metadata_based_on_access_type( - self.service_class.remove_contact_info_metadata(research_dataset)) + research_dataset = ( + self.service_class.check_and_remove_metadata_based_on_access_type( + self.service_class.remove_contact_info_metadata(research_dataset) + ) + ) return Response(data=research_dataset, status=status.HTTP_200_OK) - @action(detail=True, methods=['get'], url_path="metadata_versions") + @action(detail=True, methods=["get"], url_path="metadata_versions") def metadata_versions_list(self, request, pk=None): """ List all research_dataset version associated with this dataset. @@ -146,31 +164,35 @@ def metadata_versions_list(self, request, pk=None): entries = cr.get_metadata_version_listing() return Response(data=entries, status=status.HTTP_200_OK) - @action(detail=True, methods=['get'], url_path="files") + @action(detail=True, methods=["get"], url_path="files") def files_list(self, request, pk=None): """ Get files associated to this dataset. Can be used to retrieve a list of only deleted files by providing the query parameter removed_files=true. """ params = {} - manager = 'objects' + manager = "objects" # TODO: This applies only to IDA files, not remote resources. # TODO: Should this apply also to remote resources? cr = self.get_object() if not cr.authorized_to_see_catalog_record_files(request): - raise Http403({ - 'detail': ['You do not have permission to see this information because the dataset access type ' - 'is not open and you are not the owner of the catalog record.'] - }) - - if CS.get_boolean_query_param(request, 'removed_files'): - params['removed'] = True - manager = 'objects_unfiltered' + raise Http403( + { + "detail": [ + "You do not have permission to see this information because the dataset access type " + "is not open and you are not the owner of the catalog record." + ] + } + ) + + if CS.get_boolean_query_param(request, "removed_files"): + params["removed"] = True + manager = "objects_unfiltered" file_fields = [] - if 'file_fields' in request.query_params: - file_fields = request.query_params['file_fields'].split(',') + if "file_fields" in request.query_params: + file_fields = request.query_params["file_fields"].split(",") file_fields = LightFileSerializer.ls_field_list(file_fields) queryset = cr.files(manager=manager).filter(**params).values(*file_fields) @@ -178,31 +200,45 @@ def files_list(self, request, pk=None): return Response(data=files, status=status.HTTP_200_OK) - @action(detail=False, methods=['get'], url_path="identifiers") + @action(detail=False, methods=["get"], url_path="identifiers") def get_all_identifiers(self, request): - self.queryset_search_params = self.service_class.get_queryset_search_params(request) - q = self.get_queryset().values('identifier') - identifiers = [item['identifier'] for item in q] + self.queryset_search_params = self.service_class.get_queryset_search_params( + request + ) + q = self.get_queryset().values("identifier") + identifiers = [item["identifier"] for item in q] return Response(identifiers) - @action(detail=False, methods=['get'], url_path="metadata_version_identifiers") + @action(detail=False, methods=["get"], url_path="metadata_version_identifiers") def get_all_metadata_version_identifiers(self, request): # todo probably remove at some point - self.queryset_search_params = self.service_class.get_queryset_search_params(request) - q = self.get_queryset().values('research_dataset') - identifiers = [item['research_dataset']['metadata_version_identifier'] for item in q] + self.queryset_search_params = self.service_class.get_queryset_search_params( + request + ) + q = self.get_queryset().values("research_dataset") + identifiers = [ + item["research_dataset"]["metadata_version_identifier"] for item in q + ] return Response(identifiers) - @action(detail=False, methods=['get'], url_path="unique_preferred_identifiers") + @action(detail=False, methods=["get"], url_path="unique_preferred_identifiers") def get_all_unique_preferred_identifiers(self, request): - self.queryset_search_params = self.service_class.get_queryset_search_params(request) - - if CS.get_boolean_query_param(request, 'latest'): - queryset = self.get_queryset().filter(next_dataset_version_id=None).values('research_dataset') + self.queryset_search_params = self.service_class.get_queryset_search_params( + request + ) + + if CS.get_boolean_query_param(request, "latest"): + queryset = ( + self.get_queryset() + .filter(next_dataset_version_id=None) + .values("research_dataset") + ) else: - queryset = self.get_queryset().values('research_dataset') + queryset = self.get_queryset().values("research_dataset") - unique_pref_ids = list(set(item['research_dataset']['preferred_identifier'] for item in queryset)) + unique_pref_ids = list( + set(item["research_dataset"]["preferred_identifier"] for item in queryset) + ) return Response(unique_pref_ids) def _search_using_dataset_identifiers(self): @@ -213,13 +249,22 @@ def _search_using_dataset_identifiers(self): """ lookup_value = self.kwargs.get(self.lookup_field, False) - if 'preferred_identifier' in self.request.query_params and self.request.method == 'GET': + if ( + "preferred_identifier" in self.request.query_params + and self.request.method == "GET" + ): # search by preferred_identifier only for GET requests, while preferring: # - hits from att catalogs (assumed to be first created. improve logic if situation changes) # - first created (the first harvested occurrence, probably) # note: cant use get_object(), because get_object() will throw an error if there are multiple results - obj = self.get_queryset().filter(research_dataset__contains={'preferred_identifier': lookup_value}) \ - .order_by('data_catalog_id', 'date_created').first() + obj = ( + self.get_queryset() + .filter( + research_dataset__contains={"preferred_identifier": lookup_value} + ) + .order_by("data_catalog_id", "date_created") + .first() + ) if obj: self.check_object_permissions(self.request, obj) return obj @@ -229,46 +274,59 @@ def _search_using_dataset_identifiers(self): # todo probably remove this at some point. for now, doesnt do harm and does not instantly break # services using this... return super(DatasetViewSet, self).get_object( - search_params={ 'research_dataset__contains': {'metadata_version_identifier': lookup_value} }) + search_params={ + "research_dataset__contains": { + "metadata_version_identifier": lookup_value + } + } + ) except Http404: pass - return super(DatasetViewSet, self).get_object(search_params={ 'identifier': lookup_value }) + return super(DatasetViewSet, self).get_object( + search_params={"identifier": lookup_value} + ) - @action(detail=True, methods=['get'], url_path="redis") - def redis_test(self, request, pk=None): # pragma: no cover - if request.user.username != 'metax': + @action(detail=True, methods=["get"], url_path="redis") + def redis_test(self, request, pk=None): # pragma: no cover + if request.user.username != "metax": raise Http403() try: - cached = self.cache.get('cr-1211%s' % pk) + cached = self.cache.get("cr-1211%s" % pk) except: - _logger.debug('redis: could not connect during read') + _logger.debug("redis: could not connect during read") cached = None raise if cached: - _logger.debug('found in cache, returning') + _logger.debug("found in cache, returning") return Response(data=cached, status=status.HTTP_200_OK) data = self.get_serializer(self.object.objects.get(pk=1)).data try: - self.cache.set('cr-1211%s' % pk, data) + self.cache.set("cr-1211%s" % pk, data) except: - _logger.debug('redis: could not connect during write') + _logger.debug("redis: could not connect during write") raise return Response(data=data, status=status.HTTP_200_OK) - @action(detail=True, methods=['get'], url_path="rabbitmq") - def rabbitmq_test(self, request, pk=None): # pragma: no cover - if request.user.username != 'metax': + @action(detail=True, methods=["get"], url_path="rabbitmq") + def rabbitmq_test(self, request, pk=None): # pragma: no cover + if request.user.username != "metax": raise Http403() - rabbitmq.publish({ 'msg': 'hello create'}, routing_key='create', exchange='datasets') - rabbitmq.publish({ 'msg': 'hello update'}, routing_key='update', exchange='datasets') + rabbitmq.publish( + {"msg": "hello create"}, routing_key="create", exchange="datasets" + ) + rabbitmq.publish( + {"msg": "hello update"}, routing_key="update", exchange="datasets" + ) return Response(data={}, status=status.HTTP_200_OK) - @action(detail=False, methods=['get'], url_path="update_cr_total_files_byte_sizes") # pragma: no cover + @action( + detail=False, methods=["get"], url_path="update_cr_total_files_byte_sizes" + ) # pragma: no cover def update_cr_total_files_byte_sizes(self, request): """ Meant only for updating test data having wrong total ida byte size @@ -276,23 +334,29 @@ def update_cr_total_files_byte_sizes(self, request): :param request: :return: """ - if request.user.username != 'metax': + if request.user.username != "metax": raise Http403() # Get all IDs for ida data catalogs ida_catalog_ids = [] - for dc in DataCatalog.objects.filter(catalog_json__contains={'research_dataset_schema': 'ida'}): + for dc in DataCatalog.objects.filter( + catalog_json__contains={"research_dataset_schema": "ida"} + ): ida_catalog_ids.append(dc.id) # Update IDA CR total_files_byte_size field value without creating a new version # Skip CatalogRecord save since it prohibits changing the value of total_files_byte_size for cr in self.object.objects.filter(data_catalog_id__in=ida_catalog_ids): - cr.research_dataset['total_files_byte_size'] = sum(f.byte_size for f in cr.files.all()) + cr.research_dataset["total_files_byte_size"] = sum( + f.byte_size for f in cr.files.all() + ) cr.preserve_version = True super(Common, cr).save() return Response(data={}, status=status.HTTP_200_OK) - @action(detail=False, methods=['get'], url_path="update_cr_directory_browsing_data") # pragma: no cover + @action( + detail=False, methods=["get"], url_path="update_cr_directory_browsing_data" + ) # pragma: no cover def update_cr_directory_browsing_data(self, request): """ Meant only for updating test data: Updates cr field _directory_data with cr specific @@ -301,16 +365,19 @@ def update_cr_directory_browsing_data(self, request): :param request: :return: """ - if request.user.username != 'metax': + if request.user.username != "metax": raise Http403() - if 'id' in request.query_params: + if "id" in request.query_params: # in order to update one record only, use query param ?id=integer. useful for testcases - records = self.object.objects.filter(pk=request.query_params['id'], deprecated=False).only('id') + records = self.object.objects.filter( + pk=request.query_params["id"], deprecated=False + ).only("id") else: - records = self.object.objects \ - .filter(data_catalog__catalog_json__research_dataset_schema='ida', deprecated=False) \ - .only('id') + records = self.object.objects.filter( + data_catalog__catalog_json__research_dataset_schema="ida", + deprecated=False, + ).only("id") from time import time @@ -319,61 +386,65 @@ def update_cr_directory_browsing_data(self, request): cr.calculate_directory_byte_sizes_and_file_counts() end = time() file_count = cr.files.all().count() - dir_count = cr.files.all().distinct('parent_directory_id').count() - _logger.info('record %d took %.2f seconds. record has %d files in approximately %d directories.' % - (cr.id, end - start, file_count, dir_count)) + dir_count = cr.files.all().distinct("parent_directory_id").count() + _logger.info( + "record %d took %.2f seconds. record has %d files in approximately %d directories." + % (cr.id, end - start, file_count, dir_count) + ) return Response(data={}, status=status.HTTP_200_OK) - @action(detail=False, methods=['post'], url_path="list") + @action(detail=False, methods=["post"], url_path="list") def list_datasets(self, request): """ Returns datasets based on list of dataset IDs comming with request body. """ ids = self.service_class.identifiers_to_ids(request.data) - self.queryset_search_params = {'id__in': ids} + self.queryset_search_params = {"id__in": ids} return super(DatasetViewSet, self).list(request) + def destroy_bulk(self, request, *args, **kwargs): + return self.service_class.destroy_bulk(request) + @action(detail=False, methods=['post'], url_path="flush_password") def flush_password(self, request): # pragma: no cover """ Set a password for flush api """ - if request.user.username == 'metax': - with open('/home/metax-user/flush_password', 'w') as f: + if request.user.username == "metax": + with open("/home/metax-user/flush_password", "w") as f: dump(request.data, f) else: raise Http403 - _logger.debug('FLUSH password set') + _logger.debug("FLUSH password set") return Response(data=None, status=status.HTTP_204_NO_CONTENT) - @action(detail=False, methods=['post'], url_path="flush") - def flush_records(self, request): # pragma: no cover + @action(detail=False, methods=["post"], url_path="flush") + def flush_records(self, request): # pragma: no cover """ Delete all catalog records and files. Requires a password """ - with open('/home/metax-user/app_config') as app_config: - app_config_dict = yaml.load(app_config, Loader=yaml.FullLoader) - for host in app_config_dict['ALLOWED_HOSTS']: - if 'metax.csc.local' in host or 'metax-test' in host or 'metax-stable' in host: - - if 'password' in request.data: - with open('/home/metax-user/flush_password', 'rb') as f: - if request.data['password'] == load(f)['password']: - break - raise Http403 - else: - raise Http403 - - for f in File.objects_unfiltered.all(): - super(Common, f).delete() - - for dr in Directory.objects_unfiltered.all(): - super(Common, dr).delete() - - for f in self.object.objects_unfiltered.all(): - super(Common, f).delete() - - _logger.debug('FLUSH called by %s' % request.user.username) - - return Response(data=None, status=status.HTTP_204_NO_CONTENT) + if any( + x in settings.ALLOWED_HOSTS + for x in [ + "metax.csc.local", + "metax-test", + "metax-stable", + "localhost", + "127.0.0.1", + ] + ): + if "password" in request.data: + if request.data["password"] == env("flush_password"): + for f in File.objects_unfiltered.all(): + super(Common, f).delete() + + for dr in Directory.objects_unfiltered.all(): + super(Common, dr).delete() + + for f in self.object.objects_unfiltered.all(): + super(Common, f).delete() + + _logger.debug("FLUSH called by %s" % request.user.username) + return Response(data=None, status=status.HTTP_204_NO_CONTENT) + return Response(data=None, status=status.HTTP_403_FORBIDDEN) diff --git a/src/metax_api/api/rest/base/views/directory_view.py b/src/metax_api/api/rest/base/views/directory_view.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/views/file_storage_view.py b/src/metax_api/api/rest/base/views/file_storage_view.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/views/file_view.py b/src/metax_api/api/rest/base/views/file_view.py old mode 100644 new mode 100755 index 1fe3aaf9..01838e5b --- a/src/metax_api/api/rest/base/views/file_view.py +++ b/src/metax_api/api/rest/base/views/file_view.py @@ -24,7 +24,6 @@ from .common_view import CommonViewSet from ..serializers import FileSerializer, XmlMetadataSerializer - _logger = logging.getLogger(__name__) # i.e. /rest/v6/files, but must NOT end in / diff --git a/src/metax_api/api/rest/base/views/schema_view.py b/src/metax_api/api/rest/base/views/schema_view.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/base/xslt/datacite.xslt b/src/metax_api/api/rest/base/xslt/datacite.xslt old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/__init__.py b/src/metax_api/api/rest/v2/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/api_schemas/catalogrecord.json b/src/metax_api/api/rest/v2/api_schemas/catalogrecord.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/api_schemas/datacatalog.json b/src/metax_api/api/rest/v2/api_schemas/datacatalog.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/api_schemas/dataset_files_schema.json b/src/metax_api/api/rest/v2/api_schemas/dataset_files_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/api_schemas/file.json b/src/metax_api/api/rest/v2/api_schemas/file.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json b/src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/schemas/__init__.py b/src/metax_api/api/rest/v2/schemas/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/schemas/att_dataset_schema.json b/src/metax_api/api/rest/v2/schemas/att_dataset_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/schemas/contract_schema.json b/src/metax_api/api/rest/v2/schemas/contract_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/schemas/datacatalog_schema.json b/src/metax_api/api/rest/v2/schemas/datacatalog_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/schemas/datacite_4.1_schema.json b/src/metax_api/api/rest/v2/schemas/datacite_4.1_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/schemas/dataset_files_schema.json b/src/metax_api/api/rest/v2/schemas/dataset_files_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/schemas/dft_dataset_schema.json b/src/metax_api/api/rest/v2/schemas/dft_dataset_schema.json old mode 100644 new mode 100755 index b0fa726a..3f4449b3 --- a/src/metax_api/api/rest/v2/schemas/dft_dataset_schema.json +++ b/src/metax_api/api/rest/v2/schemas/dft_dataset_schema.json @@ -285,6 +285,45 @@ ], "additionalProperties": true }, + "Checksum":{ + "title":"Checksum", + "type":"object", + "@id":"http://spdx.org/rdf/terms#Checksum", + "description":"", + "minProperties":1, + "properties":{ + "algorithm":{ + "@id":"http://spdx.org/rdf/terms#algorithm", + "title":"Algorithm", + "description":"Used checksum algortihm", + "enum":[ + "MD5", + "SHA-1", + "SHA-224", + "SHA-256", + "SHA-384", + "SHA-512", + "OTHER" + ], + "@type":"http://www.w3.org/2001/XMLSchema#string", + "minLength":1, + "type":"string" + }, + "checksum_value":{ + "@id":"http://spdx.org/rdf/terms#checksumValue", + "title":"Checksum value", + "description":"Value of the checksum. xsd:hexBinary", + "@type":"http://www.w3.org/2001/XMLSchema#hexBinary", + "minLength":1, + "type":"string" + } + }, + "required":[ + "checksum_value", + "algorithm" + ], + "additionalProperties": false + }, "Concept":{ "title":"Concept", "type":"object", @@ -482,6 +521,54 @@ "identifier" ] }, + "DistributionCharacteristics":{ + "title":"File characteristics", + "type":"object", + "@id":"http://www.loc.gov/premis/rdf/v1#ObjectCharasteristics", + "description":"Technical properties of files", + "minProperties":1, + "properties":{ + "title":{ + "@id":"http://purl.org/dc/terms/title", + "title":"File Type Name", + "description":"A name of the file type", + "@type":"http://www.w3.org/2001/XMLSchema#string", + "minLength":1, + "type":"string" + }, + "description":{ + "@id":"http://purl.org/dc/terms/description", + "title":"File Type Description", + "description":"Description of the file type", + "@type":"http://www.w3.org/2001/XMLSchema#string", + "minLength":1, + "type":"string" + }, + "application_name":{ + "@id":"http://www.loc.gov/premis/rdf/v1#hasCreatingApplicationName", + "title":"Application name", + "description":"A designation for the name of the software program that created the object", + "@type":"http://www.w3.org/2001/XMLSchema#string", + "minLength":1, + "type":"string", + "example":[ + "MSWord" + ] + }, + "encoding":{ + "@id":"http://schema.org/encoding", + "title":"Text encoding", + "description":"Used text encoding", + "@type":"http://www.w3.org/2001/XMLSchema#string", + "minLength":1, + "type":"string" + } + }, + "required":[ + "title" + ], + "additionalProperties": false + }, "EntityRelation":{ "title":"Entity relation", "type":"object", @@ -1349,6 +1436,17 @@ "$ref":"#/definitions/Concept" } }, + "remote_resources":{ + "@id":"http://www.w3.org/ns/dcat#distribution", + "title":"Remote resources", + "description":"A concrete storage or expression format for the data in the dataset, for example a file, a database or a query interface to the data.", + "@type":"@id", + "type":"array", + "items":{ + "type":"object", + "$ref":"#/definitions/WebResource" + } + }, "access_rights":{ "@id":"http://www.w3.org/ns/dcat#accessRights", "title":"Access rights", @@ -1425,6 +1523,14 @@ "@type":"http://www.w3.org/2001/XMLSchema#integer", "type":"integer", "readOnly": true + }, + "total_remote_resources_byte_size":{ + "@id":"http://uri.suomi.fi/datamodel/ns/mrd#totalremoteresourcesbytesize", + "title":"Total remote resources byte size", + "description":"Sum of all remote resources distribution byte size. Read only field.", + "@type":"http://www.w3.org/2001/XMLSchema#integer", + "type":"integer", + "readOnly": true } }, "required":[ @@ -1611,6 +1717,139 @@ ], "additionalProperties": false }, + "WebResource":{ + "title":"Remote Web Resource", + "type":"object", + "@id":"http://www.w3.org/ns/dcat#Distribution", + "description":"Represents single available resource in the web.", + "minProperties":1, + "properties":{ + "identifier":{ + "@id":"http://purl.org/dc/terms/identifier", + "title":"Identifier", + "description":"Unique identifier for the distribution from file: scheme", + "@type":"@id", + "minLength":1, + "type":"string", + "format":"uri", + "example":[ + "file://etc/data/abc.txt" + ] + }, + "title":{ + "@id":"http://purl.org/dc/terms/title", + "title":"Title", + "description":"A name given to the distribution.", + "@type":"http://www.w3.org/2001/XMLSchema#string", + "minLength":1, + "type":"string" + }, + "description":{ + "@id":"http://purl.org/dc/terms/description", + "title":"Description", + "description":"Free-text account of the distribution.", + "@type":"http://www.w3.org/2001/XMLSchema#string", + "minLength":1, + "type":"string" + }, + "modified":{ + "@id":"http://purl.org/dc/terms/modified", + "title":"Date Modified", + "description":"Date on which the resource was changed.", + "@type":"http://www.w3.org/2001/XMLSchema#dateTime", + "minLength":1, + "type":"string", + "format":"date-time" + }, + "byte_size":{ + "@id":"http://www.w3.org/ns/dcat#byteSize", + "title":"Byte size", + "description":"The size of a distribution in bytes.", + "@type":"http://www.w3.org/2001/XMLSchema#integer", + "type":"integer" + }, + "access_url":{ + "@id":"http://www.w3.org/ns/dcat#accessURL", + "title":"Access URL", + "description":"Could be any kind of URL that gives access to a distribution of the dataset. E.g. landing page download, feed URL, SPARQL endpoint. Use when your catalog does not have information on which it is or when it is definitely not a download.", + "@type":"@id", + "type":"object", + "$ref":"#/definitions/Document" + }, + "download_url":{ + "@id":"http://www.w3.org/ns/dcat#downloadURL", + "title":"Download URL", + "description":"This is a direct link to a downloadable file in a given format. E.g. CSV file or RDF file. The format is described by the distribution's dc:format and/or dcat:mediaType", + "@type":"@id", + "type":"object", + "$ref":"#/definitions/Document" + }, + "checksum":{ + "@id":"http://spdx.org/rdf/terms#checksum", + "title":"Checksum", + "description":"Checksum of the manifestation", + "@type":"@id", + "type":"object", + "$ref":"#/definitions/Checksum" + }, + "license":{ + "@id":"http://purl.org/dc/terms/license", + "title":"License", + "description":"A legal document giving official permission to do something with the resource.", + "@type":"@id", + "type":"array", + "items":{ + "type":"object", + "$ref":"#/definitions/ResearchDataLicenseDocument" + } + }, + "file_type":{ + "@id":"http://purl.org/dc/terms/type", + "title":"File type", + "description":"File type from reference data", + "@type":"@id", + "type":"object", + "$ref":"#/definitions/Concept" + }, + "resource_type":{ + "@id":"http://uri.suomi.fi/datamodel/ns/mrd#resourcetype", + "title":"Resource type", + "description":"type of the resource", + "@type":"@id", + "type":"object", + "$ref":"#/definitions/Concept" + }, + "use_category":{ + "@id":"http://uri.suomi.fi/datamodel/ns/mrd#usetype", + "title":"Use category", + "description":"Use type of the resource", + "@type":"@id", + "type":"object", + "$ref":"#/definitions/Concept" + }, + "has_object_characteristics":{ + "@id":"http://www.loc.gov/premis/rdf/v1#hasObjectCharacteristics", + "title":"File charasteristics", + "description":"Fileformat specific charasteristics", + "@type":"@id", + "type":"object", + "$ref":"#/definitions/DistributionCharacteristics" + }, + "mediatype":{ + "@id":"http://purl.org/dc/terms/format", + "title":"File format", + "description":"Mediatype as text from https://www.iana.org/assignments/media-types/", + "@type":"http://www.w3.org/2001/XMLSchema#string", + "minLength": 1, + "type":"string" + } + }, + "required":[ + "use_category", + "title" + ], + "additionalProperties": false + }, "langString":{ "type":"object", "minProperties": 1, diff --git a/src/metax_api/api/rest/v2/schemas/file_schema.json b/src/metax_api/api/rest/v2/schemas/file_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/schemas/harvester_dataset_schema.json b/src/metax_api/api/rest/v2/schemas/harvester_dataset_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/schemas/ida_dataset_schema.json b/src/metax_api/api/rest/v2/schemas/ida_dataset_schema.json old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/serializers/__init__.py b/src/metax_api/api/rest/v2/serializers/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/serializers/catalog_record_serializer.py b/src/metax_api/api/rest/v2/serializers/catalog_record_serializer.py old mode 100644 new mode 100755 index 9a26c654..c58b525c --- a/src/metax_api/api/rest/v2/serializers/catalog_record_serializer.py +++ b/src/metax_api/api/rest/v2/serializers/catalog_record_serializer.py @@ -5,8 +5,8 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from copy import deepcopy import logging +from copy import deepcopy from os import path from jsonschema import Draft4Validator, RefResolver @@ -22,7 +22,6 @@ RedisCacheService as cache, ) - _logger = logging.getLogger(__name__) @@ -127,3 +126,21 @@ def validate_research_dataset_files(self, value): raise ValidationError({ 'detail': ['%s. Json path: %s. Schema: %s' % (e.message, [p for p in e.path], e.schema)] }) + + def _set_dataset_schema(self): + if self._validate_as_draft(): + # drafts only exists for V2 records, otherwise normal rules apply + schema_prefix = 'dft' + self.json_schema = CS.get_json_schema(self._schemas_directory_path, 'dataset', schema_prefix) + + else: + super()._set_dataset_schema() + + def _validate_as_draft(self): + if self._operation_is_create and "request" in self.context: + return CS.get_boolean_query_param(self.context["request"], 'draft') + + if CS.request_is_create_operation(self.instance.request): + return self.instance._save_as_draft() + + return True diff --git a/src/metax_api/api/rest/v2/serializers/data_catalog_serializer.py b/src/metax_api/api/rest/v2/serializers/data_catalog_serializer.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/views/__init__.py b/src/metax_api/api/rest/v2/views/__init__.py old mode 100644 new mode 100755 index 0f7078b9..112f422e --- a/src/metax_api/api/rest/v2/views/__init__.py +++ b/src/metax_api/api/rest/v2/views/__init__.py @@ -5,5 +5,5 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from .dataset_view import DatasetViewSet from .data_catalog_view import DataCatalogViewSet +from .dataset_view import DatasetViewSet diff --git a/src/metax_api/api/rest/v2/views/data_catalog_view.py b/src/metax_api/api/rest/v2/views/data_catalog_view.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rest/v2/views/dataset_view.py b/src/metax_api/api/rest/v2/views/dataset_view.py old mode 100644 new mode 100755 index 04983c79..df8182a7 --- a/src/metax_api/api/rest/v2/views/dataset_view.py +++ b/src/metax_api/api/rest/v2/views/dataset_view.py @@ -12,11 +12,11 @@ from rest_framework.decorators import action from rest_framework.response import Response -from metax_api.exceptions import Http400, Http403 -from metax_api.models import CatalogRecordV2 -from metax_api.api.rest.base.views import DatasetViewSet from metax_api.api.rest.base.serializers import LightFileSerializer +from metax_api.api.rest.base.views import DatasetViewSet from metax_api.api.rest.v2.serializers import CatalogRecordSerializerV2 +from metax_api.exceptions import Http400, Http403 +from metax_api.models import CatalogRecordV2 from metax_api.services import CommonService as CS, CatalogRecordServiceV2 _logger = logging.getLogger(__name__) diff --git a/src/metax_api/api/rpc/base/__init__.py b/src/metax_api/api/rpc/base/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rpc/base/views/__init__.py b/src/metax_api/api/rpc/base/views/__init__.py old mode 100644 new mode 100755 index d1dfc900..4dd20435 --- a/src/metax_api/api/rpc/base/views/__init__.py +++ b/src/metax_api/api/rpc/base/views/__init__.py @@ -6,6 +6,6 @@ # :license: MIT from .dataset_rpc import DatasetRPC +from .elasticsearch_rpc import ElasticsearchRPC from .file_rpc import FileRPC from .statistic_rpc import StatisticRPC -from .elasticsearch_rpc import ElasticsearchRPC diff --git a/src/metax_api/api/rpc/base/views/common_rpc.py b/src/metax_api/api/rpc/base/views/common_rpc.py old mode 100644 new mode 100755 index ade5727c..5f8f638b --- a/src/metax_api/api/rpc/base/views/common_rpc.py +++ b/src/metax_api/api/rpc/base/views/common_rpc.py @@ -7,11 +7,11 @@ import logging +from metax_api.api.rest.base.serializers import FileSerializer from metax_api.api.rest.base.views import CommonViewSet -from metax_api.models import File from metax_api.exceptions import Http501 +from metax_api.models import File from metax_api.permissions import EndUserPermissions, ServicePermissions -from metax_api.api.rest.base.serializers import FileSerializer _logger = logging.getLogger(__name__) diff --git a/src/metax_api/api/rpc/base/views/dataset_rpc.py b/src/metax_api/api/rpc/base/views/dataset_rpc.py old mode 100644 new mode 100755 index 10618f30..f13ae5af --- a/src/metax_api/api/rpc/base/views/dataset_rpc.py +++ b/src/metax_api/api/rpc/base/views/dataset_rpc.py @@ -5,8 +5,8 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from json import load import logging +from json import load from django.conf import settings as django_settings from django.http import Http404 diff --git a/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py b/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py old mode 100644 new mode 100755 index bae36547..fac38b28 --- a/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py +++ b/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py @@ -12,9 +12,9 @@ from rest_framework.decorators import action from rest_framework.response import Response -from .common_rpc import CommonRPC from metax_api.exceptions import Http400 from metax_api.utils.reference_data_loader import ReferenceDataLoader as RDL +from .common_rpc import CommonRPC _logger = logging.getLogger(__name__) diff --git a/src/metax_api/api/rpc/base/views/file_rpc.py b/src/metax_api/api/rpc/base/views/file_rpc.py old mode 100644 new mode 100755 index ff0486f4..467e4819 --- a/src/metax_api/api/rpc/base/views/file_rpc.py +++ b/src/metax_api/api/rpc/base/views/file_rpc.py @@ -9,9 +9,9 @@ from django.conf import settings as django_settings from django.db import connection +from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response -from rest_framework import status from metax_api.exceptions import Http400 from metax_api.services import FileService diff --git a/src/metax_api/api/rpc/base/views/statistic_rpc.py b/src/metax_api/api/rpc/base/views/statistic_rpc.py old mode 100644 new mode 100755 index 97068329..c02d1a40 --- a/src/metax_api/api/rpc/base/views/statistic_rpc.py +++ b/src/metax_api/api/rpc/base/views/statistic_rpc.py @@ -11,10 +11,9 @@ from rest_framework.decorators import action from rest_framework.response import Response -from .common_rpc import CommonRPC from metax_api.exceptions import Http400 from metax_api.services import StatisticService, CommonService as CS - +from .common_rpc import CommonRPC _logger = logging.getLogger(__name__) diff --git a/src/metax_api/api/rpc/v2/__init__.py b/src/metax_api/api/rpc/v2/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rpc/v2/router.py b/src/metax_api/api/rpc/v2/router.py index ae271638..07e4a2f8 100755 --- a/src/metax_api/api/rpc/v2/router.py +++ b/src/metax_api/api/rpc/v2/router.py @@ -24,7 +24,6 @@ from rest_framework.routers import DefaultRouter from metax_api.api.rpc.base.views import FileRPC, StatisticRPC - from .views import ( DatasetRPC, ) diff --git a/src/metax_api/api/rpc/v2/views/__init__.py b/src/metax_api/api/rpc/v2/views/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/api/rpc/v2/views/dataset_rpc.py b/src/metax_api/api/rpc/v2/views/dataset_rpc.py old mode 100644 new mode 100755 index 5a7eddb4..752b7447 --- a/src/metax_api/api/rpc/v2/views/dataset_rpc.py +++ b/src/metax_api/api/rpc/v2/views/dataset_rpc.py @@ -12,12 +12,11 @@ from rest_framework.decorators import action from rest_framework.response import Response -from metax_api.api.rpc.base.views import DatasetRPC from metax_api.api.rest.v2.serializers import CatalogRecordSerializerV2 +from metax_api.api.rpc.base.views import DatasetRPC from metax_api.exceptions import Http400, Http501 from metax_api.models import CatalogRecordV2 - _logger = logging.getLogger(__name__) diff --git a/src/metax_api/checks.py b/src/metax_api/checks.py new file mode 100755 index 00000000..2e26a308 --- /dev/null +++ b/src/metax_api/checks.py @@ -0,0 +1,47 @@ +import logging + +from watchman.decorators import check + +from metax_api.services.redis_cache_service import RedisClient +from metax_api.tasks.refdata.refdata_indexer.service.elasticsearch_service import ( + ElasticSearchService, +) + +logger = logging.getLogger(__name__) + + +@check +def elasticsearch_check(): + try: + es = ElasticSearchService() + ref_index = es.index_exists("reference_data") + org_data = es.index_exists("organization_data") + return { + "elasticsearch": [ + {"index: reference_data": {"ok": ref_index}}, + {"index: organization_data": {"ok": org_data}}, + ] + } + except Exception as e: + logger.error(e) + return { + "elasticsearch": { + "ok": False, + "error": str(e), + "traceback": str(e.__traceback__), + } + } + + +@check +def redis_check(): + try: + redis = RedisClient() + refdata = redis.get("reference_data") + if len(refdata) > 0: + return {"redis": [{"key: reference_data": {"ok": True}}]} + except Exception as e: + logger.error(e) + return { + "redis": {"ok": False, "error": str(e), "traceback": str(e.__traceback__)} + } diff --git a/src/metax_api/exampledata/dataset_minimal.json b/src/metax_api/exampledata/dataset_minimal.json old mode 100644 new mode 100755 diff --git a/src/metax_api/exceptions/__init__.py b/src/metax_api/exceptions/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/exceptions/http_exceptions.py b/src/metax_api/exceptions/http_exceptions.py old mode 100644 new mode 100755 diff --git a/src/metax_api/initialdata/datacatalogs.json b/src/metax_api/initialdata/datacatalogs.json old mode 100644 new mode 100755 index 378f85fa..079f7c08 --- a/src/metax_api/initialdata/datacatalogs.json +++ b/src/metax_api/initialdata/datacatalogs.json @@ -362,7 +362,7 @@ }, { "catalog_json": { - "logo": "https://www.jyu.fi/fi/yliopistopalvelut/viestintapalvelut/logot/jyu-keskitetty-kaksikielinen.jpg", + "logo": "jyu_keskitetty_kaksikielinen.png", "title": { "en": "JYU catalog", "fi": "JYU katalogi", diff --git a/src/metax_api/initialdata/filestorages.json b/src/metax_api/initialdata/filestorages.json old mode 100644 new mode 100755 diff --git a/src/metax_api/management/__init__.py b/src/metax_api/management/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/management/commands/__init__.py b/src/metax_api/management/commands/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/management/commands/index_refdata.py b/src/metax_api/management/commands/index_refdata.py new file mode 100755 index 00000000..b8512267 --- /dev/null +++ b/src/metax_api/management/commands/index_refdata.py @@ -0,0 +1,8 @@ +from django.core.management.base import BaseCommand + +from metax_api.tasks.refdata.refdata_indexer.es_index_data import index_data + + +class Command(BaseCommand): + def handle(self, *args, **options): + index_data() diff --git a/src/metax_api/management/commands/loadinitialdata.py b/src/metax_api/management/commands/loadinitialdata.py old mode 100644 new mode 100755 index 516076ef..83bcf43c --- a/src/metax_api/management/commands/loadinitialdata.py +++ b/src/metax_api/management/commands/loadinitialdata.py @@ -6,11 +6,12 @@ # :license: MIT import json + import requests -import yaml import urllib3 - +from django.conf import settings from django.core.management.base import BaseCommand, CommandError +from icecream import ic from metax_api.utils import executing_test_case from django.conf import settings as django_settings @@ -20,18 +21,19 @@ class Command(BaseCommand): - help = 'Load initial data for Metax: Data catalogs, file storages.' + help = "Load initial data for Metax: Data catalogs, file storages." def add_arguments(self, parser): # optional arguments to pass in special settings during test case execution parser.add_argument( - '--test-settings', + "--test-settings", type=str, - nargs='?', - help='Set url to use during test execution', + nargs="?", + help="Set url to use during test execution", ) def handle(self, *args, **options): + ic(executing_test_case()) if executing_test_case(): self._set_test_confs(options) else: @@ -45,39 +47,37 @@ def _set_test_confs(self, options): For test case execution, a testserver url and test metax credentials are expected as parameters from the testrunner. """ - if 'test_settings' not in options: - raise CommandError('test case execution requires settings as a parameter') - - test_opts = options['test_settings'] - self._metax_api_root = test_opts['metax_url'] - self._metax_api_user = test_opts['metax_credentials']['username'], test_opts['metax_credentials']['password'] + if "test_settings" not in options: + raise CommandError("test case execution requires settings as a parameter") + + test_opts = options["test_settings"] + self._metax_api_root = test_opts["metax_url"] + self._metax_api_user = ( + test_opts["metax_credentials"]["username"], + test_opts["metax_credentials"]["password"], + ) - if self._metax_api_root == 'https://localhost': + if self._metax_api_root == "https://localhost": # extra precaution... - raise CommandError('Test case tried to write into real db') + raise CommandError("Test case tried to write into real db") def _set_real_confs(self): """ Set metax url to localhost, and get user credentials from app_config. """ - self._metax_api_root = 'https://localhost' - try: - with open('/home/metax-user/app_config') as app_config: - app_config = yaml.load(app_config, Loader=yaml.FullLoader) - except FileNotFoundError: - raise CommandError('app_config does not exist?') - - for user in app_config['API_USERS']: - if user['username'] == 'metax': - self._metax_api_user = (user['username'], user['password']) + self._metax_api_root = settings.METAX_API_ROOT + + for user in settings.API_USERS: + if user["username"] == "metax": + self._metax_api_user = (user["username"], user["password"]) break else: - raise CommandError('Could not find metax-user from app_config ?') + raise CommandError("Could not find metax-user from app_config ?") def _error_is_already_exists(self, details): try: for field_name, errors in details.items(): - if field_name == 'identifier' and 'already exists' in errors[0]: + if field_name == "identifier" and "already exists" in errors[0]: return True except: pass @@ -85,14 +85,14 @@ def _error_is_already_exists(self, details): def _load_data_catalogs(self): try: - with open('metax_api/initialdata/datacatalogs.json', 'r') as f: + with open("metax_api/initialdata/datacatalogs.json", "r") as f: data_catalogs = json.load(f) - except FileNotFoundError: - raise CommandError('File initialdata/datacatalogs.json does not exist?') + except FileNotFoundError: # noqa + raise CommandError("File initialdata/datacatalogs.json does not exist?") except json.decoder.JSONDecodeError as e: - raise CommandError('Error loading data catalog json: %s' % str(e)) + raise CommandError("Error loading data catalog json: %s" % str(e)) - self.stdout.write('Creating %d data catalogs...' % len(data_catalogs)) + self.stdout.write("Creating %d data catalogs..." % len(data_catalogs)) for dc in data_catalogs: if dc['catalog_json']['research_dataset_schema'] == 'dft': @@ -127,9 +127,11 @@ def _load_data_catalogs(self): (self._metax_api_root, dc['catalog_json']['identifier']), json=dc, auth=self._metax_api_user, verify=False) - if response.status_code == 200: - self.stdout.write('Updated catalog: %s' % dc['catalog_json']['identifier']) - continue + if response.status_code == 200: + self.stdout.write( + "Updated catalog: %s" % dc["catalog_json"]["identifier"] + ) + continue # create or update ended in error raise CommandError('Failed to process catalog: %s. Reason: %s' % @@ -137,39 +139,56 @@ def _load_data_catalogs(self): def _load_file_storages(self): try: - with open('metax_api/initialdata/filestorages.json', 'r') as f: + with open("metax_api/initialdata/filestorages.json", "r") as f: storages = json.load(f) - except FileNotFoundError: - raise CommandError('File initialdata/filestorages.json does not exist?') + except FileNotFoundError: # noqa + raise CommandError("File initialdata/filestorages.json does not exist?") except json.decoder.JSONDecodeError as e: - raise CommandError('Error loading file storage json: %s' % str(e)) + raise CommandError("Error loading file storage json: %s" % str(e)) - self.stdout.write('Creating %d file storages...' % len(storages)) + self.stdout.write("Creating %d file storages..." % len(storages)) for fs in storages: - response = requests.post('%s/rest/filestorages' % self._metax_api_root, - json=fs, auth=self._metax_api_user, verify=False) + response = requests.post( + "%s/rest/filestorages" % self._metax_api_root, + json=fs, + auth=self._metax_api_user, + verify=False, + ) if response.status_code == 201: - self.stdout.write('Created file storage: %s' % fs['file_storage_json']['identifier']) + self.stdout.write( + "Created file storage: %s" % fs["file_storage_json"]["identifier"] + ) else: # update instead try: errors = response.json() except: raise CommandError(response.content) - if self._error_is_already_exists(errors.get('file_storage_json', {})): - self.stdout.write('File storage %s already exists, updating instead...' % - fs['file_storage_json']['identifier']) - - response = requests.put('%s/rest/filestorages/%s' % - (self._metax_api_root, fs['file_storage_json']['identifier']), - json=fs, auth=self._metax_api_user, verify=False) + if self._error_is_already_exists(errors.get("file_storage_json", {})): + self.stdout.write( + "File storage %s already exists, updating instead..." + % fs["file_storage_json"]["identifier"] + ) + + response = requests.put( + "%s/rest/filestorages/%s" + % (self._metax_api_root, fs["file_storage_json"]["identifier"]), + json=fs, + auth=self._metax_api_user, + verify=False, + ) if response.status_code == 200: - self.stdout.write('Updated file storage: %s' % fs['file_storage_json']['identifier']) + self.stdout.write( + "Updated file storage: %s" + % fs["file_storage_json"]["identifier"] + ) continue # create or update ended in error - raise CommandError('Failed to process storage: %s. Reason: %s' % - (fs['file_storage_json']['identifier'], errors)) + raise CommandError( + "Failed to process storage: %s. Reason: %s" + % (fs["file_storage_json"]["identifier"], errors) + ) diff --git a/src/metax_api/management/commands/reload_refdata_cache.py b/src/metax_api/management/commands/reload_refdata_cache.py new file mode 100755 index 00000000..4afdeb09 --- /dev/null +++ b/src/metax_api/management/commands/reload_refdata_cache.py @@ -0,0 +1,21 @@ +import logging + +from django.core.management.base import BaseCommand + +from metax_api.services.redis_cache_service import RedisClient +from metax_api.utils import ReferenceDataLoader + +_logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + def handle(self, *args, **options): + try: + cache = RedisClient() + cache.set("reference_data", None) + + ReferenceDataLoader.populate_cache_reference_data(cache) + _logger.info("event='reference_data_loaded'") + except Exception as e: + _logger.error(e) + raise e diff --git a/src/metax_api/management/commands/updatereferencedata.py b/src/metax_api/management/commands/updatereferencedata.py old mode 100644 new mode 100755 index 91a979ba..ad156002 --- a/src/metax_api/management/commands/updatereferencedata.py +++ b/src/metax_api/management/commands/updatereferencedata.py @@ -9,25 +9,25 @@ from django.core.management.base import BaseCommand, CommandError -from metax_api.services import RedisCacheService as cache +from metax_api.services.redis_cache_service import RedisClient from metax_api.utils import ReferenceDataLoader - _logger = logging.getLogger(__name__) class Command(BaseCommand): - help = 'Reload reference data to cache from ElasticSearch' + help = "Reload reference data to cache from ElasticSearch" def handle(self, *args, **options): self._update_reference_data() def _update_reference_data(self): - _logger.info('Updating reference data...') + _logger.info("Updating reference data...") try: + cache = RedisClient() ReferenceDataLoader.populate_cache_reference_data(cache) except Exception as e: - _logger.exception('Reference data update ended in an error: %s' % str(e)) + _logger.exception("Reference data update ended in an error: %s" % str(e)) raise CommandError(e) - _logger.info('Reference data updated') + _logger.info("Reference data updated") diff --git a/src/metax_api/middleware/__init__.py b/src/metax_api/middleware/__init__.py old mode 100644 new mode 100755 index 13b65045..a058918c --- a/src/metax_api/middleware/__init__.py +++ b/src/metax_api/middleware/__init__.py @@ -5,7 +5,7 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from .identifyapicaller import IdentifyApiCaller from .add_last_modified_header_to_response import AddLastModifiedHeaderToResponse +from .identifyapicaller import IdentifyApiCaller from .request_logging import RequestLogging from .stream_http_response import StreamHttpResponse diff --git a/src/metax_api/middleware/add_last_modified_header_to_response.py b/src/metax_api/middleware/add_last_modified_header_to_response.py old mode 100644 new mode 100755 index f9f3f08a..883cf498 --- a/src/metax_api/middleware/add_last_modified_header_to_response.py +++ b/src/metax_api/middleware/add_last_modified_header_to_response.py @@ -39,29 +39,33 @@ def __call__(self, request): # Code to be executed for each request/response after # the view is called. - if request.method in ['GET', 'POST', 'PUT', 'PATCH']: + if request.method in ["GET", "POST", "PUT", "PATCH"]: self._add_last_modified_header_to_response(response) return response @staticmethod def _add_last_modified_header_to_response(response): - if hasattr(response, 'data'): + if hasattr(response, "data"): obj = None if isinstance(response.data, dict): - obj = response.data.get('success', response.data) + obj = response.data.get("success", response.data) if isinstance(obj, list) and len(obj) > 0: - obj = obj[0].get('object', None) + obj = obj[0].get("object", None) modified = None if obj: - if 'date_modified' in obj: - modified = obj.get('date_modified') - elif 'date_created' in obj: - modified = obj.get('date_created') + if "date_modified" in obj: + modified = obj.get("date_modified") + elif "date_created" in obj: + modified = obj.get("date_created") if modified: modified_dt = parse_timestamp_string_to_tz_aware_datetime(modified) if modified_dt: - date_modified_in_gmt = timezone.localtime(modified_dt, timezone=tz('GMT')) - response['Last-Modified'] = date_modified_in_gmt.strftime('%a, %d %b %Y %H:%M:%S GMT') + date_modified_in_gmt = timezone.localtime( + modified_dt, timezone=tz("GMT") + ) + response["Last-Modified"] = date_modified_in_gmt.strftime( + "%a, %d %b %Y %H:%M:%S GMT" + ) diff --git a/src/metax_api/middleware/identifyapicaller.py b/src/metax_api/middleware/identifyapicaller.py old mode 100644 new mode 100755 index abc04ba5..26454ef7 --- a/src/metax_api/middleware/identifyapicaller.py +++ b/src/metax_api/middleware/identifyapicaller.py @@ -5,11 +5,10 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import json import logging from base64 import b64decode -import json -import yaml import requests from django.conf import settings as django_settings from django.http import HttpResponseForbidden @@ -39,15 +38,14 @@ """ -WRITE_METHODS = ('POST', 'PUT', 'PATCH', 'DELETE') +WRITE_METHODS = ("POST", "PUT", "PATCH", "DELETE") if not django_settings.TLS_VERIFY: requests.packages.urllib3.disable_warnings() -class _IdentifyApiCaller(): - +class _IdentifyApiCaller: def __init__(self, get_response): self.get_response = get_response self.API_USERS = self._get_api_users() @@ -77,16 +75,15 @@ def _get_api_users(self): """ Services, or other pre-defined api users. """ - with open('/home/metax-user/app_config') as app_config: - app_config_dict = yaml.load(app_config, Loader=yaml.FullLoader) + try: - return app_config_dict['API_USERS'] + return django_settings.API_USERS except: - _logger.exception('API_USERS missing from app_config') + _logger.exception("API_USERS missing from app_config") raise def _caller_should_be_identified(self, request): - if request.META.get('HTTP_AUTHORIZATION', None): + if request.META.get("HTTP_AUTHORIZATION", None): return True elif request.method in WRITE_METHODS: return True @@ -104,38 +101,47 @@ def _identify_api_caller(self, request): Valid service users and authentication methods are listed in app_config. """ - http_auth_header = request.META.get('HTTP_AUTHORIZATION', None) + http_auth_header = request.META.get("HTTP_AUTHORIZATION", None) if not http_auth_header: - _logger.warning('Unauthenticated access attempt from ip: %s. Authorization header missing' - % request.META['REMOTE_ADDR']) + _logger.warning( + "Unauthenticated access attempt from ip: %s. Authorization header missing" + % request.META["REMOTE_ADDR"] + ) raise Http403 try: - auth_method, auth_b64 = http_auth_header.split(' ') + auth_method, auth_b64 = http_auth_header.split(" ") except ValueError: - raise Http403({ - 'detail': [ - 'Invalid HTTP authorization method. Ensure you included on of the following ' - 'methods inside the auth header: %s' % ', '.join(self.ALLOWED_AUTH_METHODS) - ] - }) + raise Http403( + { + "detail": [ + "Invalid HTTP authorization method. Ensure you included on of the following " + "methods inside the auth header: %s" + % ", ".join(self.ALLOWED_AUTH_METHODS) + ] + } + ) if auth_method not in self.ALLOWED_AUTH_METHODS: - _logger.warning('Invalid HTTP authorization method: %s' % auth_method) - raise Http403({ - 'detail': [ - 'Invalid HTTP authorization method: %s. Allowed auth methods: %s' - % (auth_method, ', '.join(self.ALLOWED_AUTH_METHODS)) - ] - }) - - if auth_method.lower() == 'basic': + _logger.warning("Invalid HTTP authorization method: %s" % auth_method) + raise Http403( + { + "detail": [ + "Invalid HTTP authorization method: %s. Allowed auth methods: %s" + % (auth_method, ", ".join(self.ALLOWED_AUTH_METHODS)) + ] + } + ) + + if auth_method.lower() == "basic": self._auth_basic(request, auth_b64) - elif auth_method.lower() == 'bearer': + elif auth_method.lower() == "bearer": self._auth_bearer(request, auth_b64) else: - raise Exception('The allowed auth method %s is missing handling' % auth_method) + raise Exception( + "The allowed auth method %s is missing handling" % auth_method + ) return request @@ -144,37 +150,37 @@ def _auth_basic(self, request, auth_b64): Check request user credentials for service-level users. """ try: - username, apikey = b64decode(auth_b64).decode('utf-8').split(':') + username, apikey = b64decode(auth_b64).decode("utf-8").split(":") except: - _logger.warning('Malformed HTTP Authorization header (Basic)') - raise Http403({ 'detail': [ 'Malformed HTTP Authorization header (Basic)' ]}) + _logger.warning("Malformed HTTP Authorization header (Basic)") + raise Http403({"detail": ["Malformed HTTP Authorization header (Basic)"]}) - user = next(( u for u in self.API_USERS if u['username'] == username), None) + user = next((u for u in self.API_USERS if u["username"] == username), None) if not user: - _logger.warning('Failed authnz for user %s: user not found' % username) + _logger.warning("Failed authnz for user %s: user not found" % username) raise Http403 - if apikey != user['password']: - _logger.warning('Failed authnz for user %s: password mismatch' % username) + if apikey != user["password"]: + _logger.warning("Failed authnz for user %s: password mismatch" % username) raise Http403 request.user.username = username request.user.is_service = True def _auth_bearer(self, request, auth_b64): - _logger.debug('validating bearer token...') + _logger.debug("validating bearer token...") response = requests.get( # url protected by oidc. the proxy is configured to return 200 OK for any valid token django_settings.VALIDATE_TOKEN_URL, - headers={ 'Authorization': request.META.get('HTTP_AUTHORIZATION', None) }, - verify=False + headers={"Authorization": request.META.get("HTTP_AUTHORIZATION", None)}, + verify=False, ) - _logger.debug('response from token validation: %s' % str(response)) + _logger.debug("response from token validation: %s" % str(response)) if response.status_code != 200: - _logger.warning('Bearer token validation failed') + _logger.warning("Bearer token validation failed") raise Http403 try: @@ -182,13 +188,15 @@ def _auth_bearer(self, request, auth_b64): except: # the above method should never fail, as this code should not be # reachable if the token validation had already failed. - _logger.exception('Failed to extract token from id_token string') + _logger.exception("Failed to extract token from id_token string") raise Http403 - if len(token.get('CSCUserName', '')) > 0: - request.user.username = token['CSCUserName'] + if len(token.get("CSCUserName", "")) > 0: + request.user.username = token["CSCUserName"] else: - _logger.warning('id_token does not contain valid user id: fairdataid or cscusername') + _logger.warning( + "id_token does not contain valid user id: fairdataid or cscusername" + ) raise Http403 request.user.is_service = False @@ -199,16 +207,15 @@ def _extract_id_token(self, id_token_string): Extract the interesting part from the dot-separated string that looks something like "asdasd.abcabc.defghi". """ - id_token_payload_b64 = id_token_string.split('.')[1] + id_token_payload_b64 = id_token_string.split(".")[1] # in the dot-separated string, the b64 encoded strings may not be multiples of 4, which # complete valid b64 strings should be. add a few trailing '=' characters to ensure # requirement is satisfied. the b64decode method knows to discard excess '='. - return json.loads(b64decode('%s===' % id_token_payload_b64).decode('utf-8')) + return json.loads(b64decode("%s===" % id_token_payload_b64).decode("utf-8")) class _IdentifyApiCallerDummy(_IdentifyApiCaller): - def _get_api_users(self): return django_settings.API_TEST_USERS diff --git a/src/metax_api/middleware/request_logging.py b/src/metax_api/middleware/request_logging.py old mode 100644 new mode 100755 index 77c825a6..827b7057 --- a/src/metax_api/middleware/request_logging.py +++ b/src/metax_api/middleware/request_logging.py @@ -5,16 +5,12 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from base64 import b64decode -from json import loads as json_loads -from time import time import logging import os +from base64 import b64decode +from json import loads as json_loads -from metax_api.utils import json_logger - - -_logger = logging.getLogger('metax_api') +_logger = logging.getLogger("metax_api") SUCCESS_CODES = (200, 201, 204) @@ -24,125 +20,58 @@ """ -class RequestLogging(): - +class RequestLogging: def __init__(self, get_response): self.get_response = get_response self._pid = os.getpid() def __call__(self, request): - - start_time = time() - username, user_type = self.get_username(request) - - try: - json_logger.info( - event='request_start', - user_id=username, - request={ - 'query_string': request.environ['QUERY_STRING'], - 'url_path': request.environ['PATH_INFO'], - 'ip': request.environ['HTTP_X_REAL_IP'], - 'user_type': user_type, - 'http_method': request.environ['REQUEST_METHOD'], - 'process_id': self._pid, - } - ) - except: - _logger.exception('Exception during trying to json-log request start') - - try: - _logger.info( - '%s - [%s] %s - "%s %s %s" %s' - % ( - request.environ['HTTP_X_REAL_IP'], - user_type, - username, - request.environ['REQUEST_METHOD'], - request.get_full_path(), - request.environ['SERVER_PROTOCOL'], - request.environ.get('HTTP_USER_AGENT', '(no useragent)') - ) - ) - except: - _logger.exception('Exception during trying to log request start') + _logger.info( + f"request: {request.method} usertype: {user_type}, username: {username} pid: {self._pid}" + ) response = self.get_response(request) - try: - _logger.info( - '%s - [%s] %s - "%s %s" %d %s - %.3fs' - % ( - request.environ['HTTP_X_REAL_IP'], - user_type, - username, - request.method, - request.get_full_path(), - response.status_code, - response._headers.get('content-length', ['-', '-'])[1], - time() - start_time - ) - ) - except: - _logger.exception('Exception during trying to log request end') - - try: - request_info = { - 'event': 'request_end', - 'user_id': username, - 'request': { - 'query_string': request.environ['QUERY_STRING'], - 'url_path': request.environ['PATH_INFO'], - 'ip': request.environ['HTTP_X_REAL_IP'], - 'user_type': user_type, - 'http_method': request.environ['REQUEST_METHOD'], - 'http_status': response.status_code, - 'process_id': self._pid, - 'request_duration': float('%.3f' % (time() - start_time)), - } - } - - if response.status_code not in SUCCESS_CODES and hasattr(response, 'data') and \ - 'error_identifier' in response.data: - request_info['error'] = { 'error_identifier': response.data['error_identifier'] } - - json_logger.info(**request_info) - except: - _logger.exception('Exception during trying to json-log request end') - return response def get_username(self, request): """ Add more auth methods as necessary... """ - auth_header = request.environ.get('HTTP_AUTHORIZATION', None) - user = '' - user_type = 'guest' + auth_header = request.environ.get("HTTP_AUTHORIZATION", None) + user = "" + user_type = "guest" if not auth_header: return user, user_type - if 'Basic' in auth_header: + if "Basic" in auth_header: try: - user_type = 'service' - user = b64decode(auth_header.split(' ')[1]).decode('utf-8').split(':')[0] + user_type = "service" + user = ( + b64decode(auth_header.split(" ")[1]).decode("utf-8").split(":")[0] + ) except: - _logger.exception('Could not extract username from http auth header') - elif 'Bearer' in auth_header or 'bearer' in auth_header: + _logger.exception("Could not extract username from http auth header") + elif "Bearer" in auth_header or "bearer" in auth_header: try: - user_type = 'end_user' + user_type = "end_user" user = json_loads( - b64decode(auth_header.split(' ')[1].split('.')[1] + '===') - .decode('utf-8') - )['CSCUserName'] + b64decode(auth_header.split(" ")[1].split(".")[1] + "===").decode( + "utf-8" + ) + )["CSCUserName"] except: # dont log as an error or crash, since we dont want to get bothered by # errors about malformed tokens. auth middleware is going to reject this # token later too. - _logger.info('Faulty token: Could not extract username from bearer token') + _logger.info( + "Faulty token: Could not extract username from bearer token" + ) else: - _logger.info('HTTP Auth method not basic or bearer - unable to get username') + _logger.info( + "HTTP Auth method not basic or bearer - unable to get username" + ) return user, user_type diff --git a/src/metax_api/middleware/stream_http_response.py b/src/metax_api/middleware/stream_http_response.py old mode 100644 new mode 100755 index 66eb60f7..5ca379a9 --- a/src/metax_api/middleware/stream_http_response.py +++ b/src/metax_api/middleware/stream_http_response.py @@ -5,8 +5,8 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from json import dumps as json_dumps import logging +from json import dumps as json_dumps from django.http import StreamingHttpResponse @@ -15,7 +15,7 @@ class StreamHttpResponse(object): - _METHODS = ('GET', 'HEAD') + _METHODS = ("GET", "HEAD") def __init__(self, get_response): self.get_response = get_response @@ -31,11 +31,15 @@ def __call__(self, request): # Code to be executed for each request/response after # the view is called. - if request.method in self._METHODS and hasattr(response, 'data') \ - and isinstance(response.data, list) and self._check_query_param(request, 'stream'): + if ( + request.method in self._METHODS + and hasattr(response, "data") + and isinstance(response.data, list) + and self._check_query_param(request, "stream") + ): resp = StreamingHttpResponse(self._stream_response(response)) - resp._headers['content-type'] = ('Content-Type', 'application/json') - resp._headers['x-count'] = ('X-Count', str(len(response.data))) + resp._headers["content-type"] = ("Content-Type", "application/json") + resp._headers["x-count"] = ("X-Count", str(len(response.data))) return resp return response @@ -45,37 +49,39 @@ def _check_query_param(self, request, param): request is a gunicorn WSGI request, not a django request which has the query params all figured out already. Seek the value of a boolean query param from a string blob which represents the query parameters. """ - qs = request.META.get('QUERY_STRING', None) + qs = request.META.get("QUERY_STRING", None) if not qs: return False - elif 'stream' not in qs: + elif "stream" not in qs: return False # QUERY_STRING may look like "stuff=true&more_stuff=false&even_more_stuff_but_no_value" - params = qs.split('&') + params = qs.split("&") for p in params: if not p.startswith(param): continue - split = p.split('=') + split = p.split("=") if len(split) == 1: # only name was specified, which counts as a True return True else: - if split[1] in ('true', 'false'): - return split[1] == 'true' + if split[1] in ("true", "false"): + return split[1] == "true" return False return False def _stream_response(self, response): if response.data: - yield '[' + yield "[" if len(response.data) > 1: for item in response.data[:-1]: - yield '%s,' % (json_dumps(item) if isinstance(item, dict) else item) - yield json_dumps(response.data[-1]) if isinstance(response.data[-1], dict) else response.data[-1] - yield ']' + yield "%s," % (json_dumps(item) if isinstance(item, dict) else item) + yield json_dumps(response.data[-1]) if isinstance( + response.data[-1], dict + ) else response.data[-1] + yield "]" else: - yield '[]' + yield "[]" diff --git a/src/metax_api/migrations/0001_initial.py b/src/metax_api/migrations/0001_initial.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0002_create_json_indexes.py b/src/metax_api/migrations/0002_create_json_indexes.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0003_auto_20180830_1534.py b/src/metax_api/migrations/0003_auto_20180830_1534.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0004_auto_20180919_1502.py b/src/metax_api/migrations/0004_auto_20180919_1502.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0005_auto_20181101_1524.py b/src/metax_api/migrations/0005_auto_20181101_1524.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0006_catalogrecord_date_deprecated.py b/src/metax_api/migrations/0006_catalogrecord_date_deprecated.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0007_auto_20181126_1436.py b/src/metax_api/migrations/0007_auto_20181126_1436.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0008_remove_file_replication_path.py b/src/metax_api/migrations/0008_remove_file_replication_path.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0009_catalogrecord_preservation_identifier.py b/src/metax_api/migrations/0009_catalogrecord_preservation_identifier.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0010_auto_20190627_1032.py b/src/metax_api/migrations/0010_auto_20190627_1032.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0011_catalogrecord_preservation_dataset_version.py b/src/metax_api/migrations/0011_catalogrecord_preservation_dataset_version.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0012_auto_20190823_1055.py b/src/metax_api/migrations/0012_auto_20190823_1055.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0013_catalogrecord__access_granter.py b/src/metax_api/migrations/0013_catalogrecord__access_granter.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0014_catalogrecord_state.py b/src/metax_api/migrations/0014_catalogrecord_state.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0015_auto_20200218_1607.py b/src/metax_api/migrations/0015_auto_20200218_1607.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0016_auto_20200310_1341.py b/src/metax_api/migrations/0016_auto_20200310_1341.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0017_catalogrecord_rems_identifier.py b/src/metax_api/migrations/0017_catalogrecord_rems_identifier.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0018_auto_20200330_1101.py b/src/metax_api/migrations/0018_auto_20200330_1101.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0019_auto_20200419_2149.py b/src/metax_api/migrations/0019_auto_20200419_2149.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0020_auto_20200429_1512.py b/src/metax_api/migrations/0020_auto_20200429_1512.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0021_catalogrecord_next_draft.py b/src/metax_api/migrations/0021_catalogrecord_next_draft.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0022_auto_20200528_1758.py b/src/metax_api/migrations/0022_auto_20200528_1758.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0023_catalogrecord_api_meta.py b/src/metax_api/migrations/0023_catalogrecord_api_meta.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0024_catalogrecord_use_doi_for_published.py b/src/metax_api/migrations/0024_catalogrecord_use_doi_for_published.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0025_auto_20200811_1050.py b/src/metax_api/migrations/0025_auto_20200811_1050.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0026_auto_20200917_1615.py b/src/metax_api/migrations/0026_auto_20200917_1615.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0027_auto_20201013_1404.py b/src/metax_api/migrations/0027_auto_20201013_1404.py old mode 100644 new mode 100755 index 5719b0b4..f592f07a --- a/src/metax_api/migrations/0027_auto_20201013_1404.py +++ b/src/metax_api/migrations/0027_auto_20201013_1404.py @@ -11,26 +11,29 @@ def rename(apps, schema_editor): CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') # Loop through csv - with open('metax_api/migrations/datasets/SYKE_csv.csv', newline='') as csvfile: - reader = csv.DictReader(csvfile, delimiter=";", quotechar='"') - - for row in reader: - old_guid = row["vanha guid"] - - if old_guid == 'vanha guid': # Check if is heade value - pass - else: - # Get dataset - try: - cr = CatalogRecord.objects.get(research_dataset__other_identifier__0__notation = old_guid) - new_guid = row["uusi guid"] - cr.research_dataset['other_identifier'][0]['notation'] = new_guid - cr.research_dataset['other_identifier'][0]['old_notation'] = old_guid - cr.user_modified = "migration_0027" - cr.save() - logger.info(f"successful migration of cr {cr.id} with new guid {new_guid} and old guid {old_guid}") - except CatalogRecord.DoesNotExist: + try: + with open('metax_api/migrations/datasets/SYKE_csv.csv', newline='') as csvfile: + reader = csv.DictReader(csvfile, delimiter=";", quotechar='"') + + for row in reader: + old_guid = row["vanha guid"] + + if old_guid == 'vanha guid': # Check if is heade value pass + else: + # Get dataset + try: + cr = CatalogRecord.objects.get(research_dataset__other_identifier__0__notation = old_guid) + new_guid = row["uusi guid"] + cr.research_dataset['other_identifier'][0]['notation'] = new_guid + cr.research_dataset['other_identifier'][0]['old_notation'] = old_guid + cr.user_modified = "migration_0027" + cr.save() + logger.info(f"successful migration of cr {cr.id} with new guid {new_guid} and old guid {old_guid}") + except CatalogRecord.DoesNotExist: + pass + except FileNotFoundError as e: + logger.error(f"syke csv not found: {e}") def revert(apps, schema_editor): CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') diff --git a/src/metax_api/migrations/0028_auto_20201020_1010.py b/src/metax_api/migrations/0028_auto_20201020_1010.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/0029_auto_20201028_1602.py b/src/metax_api/migrations/0029_auto_20201028_1602.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/__init__.py b/src/metax_api/migrations/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/migrations/datasets/SYKE_csv.csv b/src/metax_api/migrations/datasets/SYKE_csv.csv old mode 100644 new mode 100755 index 654a3edf..0459942a --- a/src/metax_api/migrations/datasets/SYKE_csv.csv +++ b/src/metax_api/migrations/datasets/SYKE_csv.csv @@ -1,240 +1,240 @@ -title;vanha url;access;vanha guid;uusi guid;Omistaja;uusi url;; -Asuinalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E4DBC005-823B-469F-9AAB-51450438F41A};Avoin;{E4DBC005-823B-469F-9AAB-51450438F41A};{0B77FA8C-7C42-4783-AA5C-868152E32BE0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0B77FA8C-7C42-4783-AA5C-868152E32BE0};; -Chlorophyll-a daily product for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C6DCCB67-99B6-449A-A36F-26A847E6D856};Avoin;{C6DCCB67-99B6-449A-A36F-26A847E6D856};{10962669-987F-4FFA-9D73-A96E61AD6992};Finnish Environment Intitute (SYKE);https://metadata.ymparisto.fi/dataset/{10962669-987F-4FFA-9D73-A96E61AD6992};; -Chlorophyll-a daily product for the Baltic Sea 2012-2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={681B6DFF-2B4C-4881-AF69-37AD15A2B16A};Avoin;{681B6DFF-2B4C-4881-AF69-37AD15A2B16A};;;https://metadata.ymparisto.fi/dataset/;; -Chlorophyll-a weekly composite for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={422E4BF9-D15B-4779-8ABD-3610BD645C5C};Avoin;{422E4BF9-D15B-4779-8ABD-3610BD645C5C};;;https://metadata.ymparisto.fi/dataset/;; -Corine maanpeite 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A8936D40-2159-468D-84CD-0A978609B968};Avoin;{A8936D40-2159-468D-84CD-0A978609B968};{A70BC25F-43A2-4C33-A42D-9C22D9D4A3DB};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A70BC25F-43A2-4C33-A42D-9C22D9D4A3DB};; -Corine maanpeite 2006;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4438F7E1-2927-4854-B8F8-0EE8E8822C53};Avoin;{4438F7E1-2927-4854-B8F8-0EE8E8822C53};{703D5B2B-4CCC-49A7-B9E9-92B2E1290CD3};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{703D5B2B-4CCC-49A7-B9E9-92B2E1290CD3};; -Corine maanpeite 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D54C552F-F7F7-489B-8B1E-E093D93C7386};Avoin;{D54C552F-F7F7-489B-8B1E-E093D93C7386};{66D9A881-EE3C-42AD-9416-014EA6B84D23};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{66D9A881-EE3C-42AD-9416-014EA6B84D23};; -Harva ja tiheä taajama-alue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={674D6DB4-5FD9-434A-855F-F51316704630};Avoin;{674D6DB4-5FD9-434A-855F-F51316704630};{394B169F-2AE6-4966-8055-C593488F8898};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{394B169F-2AE6-4966-8055-C593488F8898};; -Havaitut tulva-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={62C30F1D-570D-4C75-80AE-6FE5402ECBBE};Avoin;{62C30F1D-570D-4C75-80AE-6FE5402ECBBE};{D8DE03E4-3F49-4BC1-9C58-3FADFB0F369A};;https://metadata.ymparisto.fi/dataset/{D8DE03E4-3F49-4BC1-9C58-3FADFB0F369A};; -Hydrologiset havaintopaikat, haihdunta-asemat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={72C2B39E-E581-4B5B-8C28-E5E0190DD97F};Avoin;{72C2B39E-E581-4B5B-8C28-E5E0190DD97F};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, jäänpaksuus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AB16A1FF-65E1-4A6E-A73D-653DDAAA7495};Avoin;{AB16A1FF-65E1-4A6E-A73D-653DDAAA7495};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, lumilinjamittaus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3D7A5268-E65C-4AB1-9A5E-99C8909AD764};Avoin;{3D7A5268-E65C-4AB1-9A5E-99C8909AD764};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, lämpötilaluotaus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={854C80BE-3383-4E67-8C21-0E4D038B7BA8};Avoin;{854C80BE-3383-4E67-8C21-0E4D038B7BA8};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, pintaveden lämpötila;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8462E50B-16BD-4C7F-9A79-F3DC6BFA1080};Avoin;{8462E50B-16BD-4C7F-9A79-F3DC6BFA1080};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, valunta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E11A44F-4987-4FF2-B55B-A3F94739FEE5};Avoin;{1E11A44F-4987-4FF2-B55B-A3F94739FEE5};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, vedenkorkeus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FC8A217-6A9D-4FE2-8923-843DA4A7DBE5};Avoin;{6FC8A217-6A9D-4FE2-8923-843DA4A7DBE5};;;https://metadata.ymparisto.fi/dataset/;; -Hydrologiset havaintopaikat, virtaama;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B6AFC4CE-7827-46BE-9372-141CE59394AA};Avoin;{B6AFC4CE-7827-46BE-9372-141CE59394AA};;;https://metadata.ymparisto.fi/dataset/;; -INSPIRE-tietotuote: Luonnonsuojelu- ja erämaa-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DFC5480E-CFC4-4FF3-B42F-9685481309BC};Avoin;{DFC5480E-CFC4-4FF3-B42F-9685481309BC};;;https://metadata.ymparisto.fi/dataset/;; -INSPIRE-tietotuote: Natura 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EB7FEE73-535C-49D8-AD0B-EAF13E52E665};Avoin;{EB7FEE73-535C-49D8-AD0B-EAF13E52E665};;;https://metadata.ymparisto.fi/dataset/;; -INSPIRE-tietotuote: Uomaverkosto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6AF05F3B-0D56-49D5-BFEA-F1652620D879};Avoin;{6AF05F3B-0D56-49D5-BFEA-F1652620D879};;;https://metadata.ymparisto.fi/dataset/;; -Itämeren päivittäiset pintalämpötilat / Sea surface temperature for the Baltic Sea;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4568E834-419F-4295-8CB1-2BE76C1F3318};Avoin;{4568E834-419F-4295-8CB1-2BE76C1F3318};;;https://metadata.ymparisto.fi/dataset/;; -Itämeren viikottaiset pintalämpötilat / Sea surface temperature weekly composites for the Baltic Sea;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1B235B6E-170C-4EF4-BE20-77C606F1A165};Avoin;{1B235B6E-170C-4EF4-BE20-77C606F1A165};;;https://metadata.ymparisto.fi/dataset/;; -Järvien ja jokien luotauspisteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={28041FC2-F80F-43FF-BEAA-CE66D449D04F};Avoin;{28041FC2-F80F-43FF-BEAA-CE66D449D04F};{80B4FB0F-C60E-4290-8183-1309E8AD8B76};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{80B4FB0F-C60E-4290-8183-1309E8AD8B76};; -Järvien ja jokien syvyysaineisto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E3647EED-2697-4EDE-92C9-900DD458B87F};Avoin;{E3647EED-2697-4EDE-92C9-900DD458B87F};{DC4AD1E9-F4E5-4998-825D-D2CA4836A2F5};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{DC4AD1E9-F4E5-4998-825D-D2CA4836A2F5};; -Kaupunki-maaseutu-luokitus (YKR);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BDD349C3-3656-4B3C-A590-77901C058DA2};Avoin;{BDD349C3-3656-4B3C-A590-77901C058DA2};{A2556B8B-0E17-4E70-AF87-0FCEBDADBE89};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A2556B8B-0E17-4E70-AF87-0FCEBDADBE89};; -Keskustat ja kaupan alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7D2F780B-39D7-4C83-9A2A-C3E3D6534B61};Avoin;{7D2F780B-39D7-4C83-9A2A-C3E3D6534B61};{79B6DD92-3448-402D-95A9-D56B484CD6A1};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{79B6DD92-3448-402D-95A9-D56B484CD6A1};; -Koskiensuojelulailla suojellut vesistöt;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={48CD102E-D5CE-445A-B9DC-74F8E9345D94};Avoin;{48CD102E-D5CE-445A-B9DC-74F8E9345D94};{CF5DEA48-E8E4-49AA-8EDB-DBE862B92F07};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CF5DEA48-E8E4-49AA-8EDB-DBE862B92F07};; -Lajit, luontodirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4E1FF620-E911-4678-83A4-F4A13944BD1A};Avoin;{4E1FF620-E911-4678-83A4-F4A13944BD1A};{4F51E747-E3DC-4223-BD0D-50108396F8A1};;https://metadata.ymparisto.fi/dataset/{4F51E747-E3DC-4223-BD0D-50108396F8A1};; -Lake Chl-a products from Finland (MERIS, FRESHMON);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=CHL_Finland_SYKE_2006-2011_MERIS_FUB_RECT;Avoin;CHL_Finland_SYKE_2006-2011_MERIS_FUB_RECT;;;https://metadata.ymparisto.fi/dataset/;; -Liikennealueiden ympäristöriskiluokitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F41D131D-B103-4726-B291-E43B49D136A2};Avoin;{F41D131D-B103-4726-B291-E43B49D136A2};{0143CB1F-4203-4252-B042-E32CB570A930};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0143CB1F-4203-4252-B042-E32CB570A930};; -Linnut, lintudirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={084531FC-EE76-418A-9832-5F8B418BA60E};Avoin;{084531FC-EE76-418A-9832-5F8B418BA60E};{EEE47DAE-72EF-403A-AED4-7F5E78E53739};;https://metadata.ymparisto.fi/dataset/{EEE47DAE-72EF-403A-AED4-7F5E78E53739};; -Luonnonsuojelu- ja erämaa-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2627E9FE-B657-48E1-A98D-000D4CD5CA38};Avoin;{2627E9FE-B657-48E1-A98D-000D4CD5CA38};{C8FC4A42-A2C3-40C4-92CD-2299C688514E};Metsähallitus;https://metadata.ymparisto.fi/dataset/{C8FC4A42-A2C3-40C4-92CD-2299C688514E};; -Luonnonsuojeluohjelma-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C305FA65-F319-4FA0-AAB8-F92AE32B6EE2};Avoin;{C305FA65-F319-4FA0-AAB8-F92AE32B6EE2};{5A93FF5B-FB88-40C7-84E0-91FCC492A621};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5A93FF5B-FB88-40C7-84E0-91FCC492A621};; -Luontotyypit, luontodirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B67BC862-E17C-42A2-B7AE-70B8A171170F};Avoin;{B67BC862-E17C-42A2-B7AE-70B8A171170F};{A0DF9C40-CC7B-4359-8595-5C76B760257C};;https://metadata.ymparisto.fi/dataset/{A0DF9C40-CC7B-4359-8595-5C76B760257C};; -Maasto- ja vesiliikenteen rajoitusalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={01A88DB7-7672-4671-95C4-3CD2AEAC5A99};Avoin;{01A88DB7-7672-4671-95C4-3CD2AEAC5A99};{AFBC7507-2167-4224-8CBA-37E1B6C975A3};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AFBC7507-2167-4224-8CBA-37E1B6C975A3};; -Maisemamaakuntajako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8D4119C7-23C6-419C-B874-2FB4C8DAEC44};Avoin;{8D4119C7-23C6-419C-B874-2FB4C8DAEC44};{27ACED25-8784-484D-962B-3207459D8BF0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{27ACED25-8784-484D-962B-3207459D8BF0};; -Metsäkasvillisuusvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={56486ECE-92E9-4FA5-96F3-0FC1D1B8C428};Avoin;{56486ECE-92E9-4FA5-96F3-0FC1D1B8C428};{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};; -Metsäkasvillisuusvyöhykkeet uhanalaisarviointia varten;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FBF9037A-3381-47C4-AD09-BBEAB007FEC0};Avoin;{FBF9037A-3381-47C4-AD09-BBEAB007FEC0};{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};;https://metadata.ymparisto.fi/dataset/{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};; -Natura2000 alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={36FA93DC-F156-427E-973E-E83F7AFC6306};Avoin;{36FA93DC-F156-427E-973E-E83F7AFC6306};{ED80465E-135B-4391-AA8A-FE2038FB224D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{ED80465E-135B-4391-AA8A-FE2038FB224D};; -Pohjavesialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F8321822-0A0D-4AC4-81E3-B9E3BE6386A2};Avoin;{F8321822-0A0D-4AC4-81E3-B9E3BE6386A2};{8F45F7BF-669F-4434-A8DB-8E686933F6FF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8F45F7BF-669F-4434-A8DB-8E686933F6FF};; -Rakennussuojelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1500436E-FD0E-4560-A933-847F16EFCC94};Avoin;{1500436E-FD0E-4560-A933-847F16EFCC94};{46AC1915-EC9D-4A02-92C0-58DAD8301E4A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{46AC1915-EC9D-4A02-92C0-58DAD8301E4A};; -Ranta10 - rantaviiva 1:10 000 ja uomaverkosto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A40A94CB-4905-4489-9C03-52B6CE9F66CD};Avoin;{A40A94CB-4905-4489-9C03-52B6CE9F66CD};{E15CD0B3-3934-4ABC-BF23-A8C622FA6A57};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E15CD0B3-3934-4ABC-BF23-A8C622FA6A57};; -Residential Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={593D9E64-4289-4B7B-A946-7A59C763C904};Avoin;{593D9E64-4289-4B7B-A946-7A59C763C904};;;https://metadata.ymparisto.fi/dataset/;; -Satelliittikuvamosaiikki (Image2000);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AF0A925D-9410-4431-8DB8-0455C73D193E};Avoin;{AF0A925D-9410-4431-8DB8-0455C73D193E};{0FD2F424-2C22-4384-A0C6-65AF19DED2A9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0FD2F424-2C22-4384-A0C6-65AF19DED2A9};; -Satelliittikuvamosaiikki (Image2006);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F1B27979-9B92-4607-B97F-28A5808AD5CC};Avoin;{F1B27979-9B92-4607-B97F-28A5808AD5CC};{5F95B099-0EAC-44DC-A1D5-B5B4A388A862};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5F95B099-0EAC-44DC-A1D5-B5B4A388A862};; -Satelliittikuvamosaiikki (Image2012);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={85FE6E30-90D2-4DB9-901B-7D86A74599DD};Avoin;{85FE6E30-90D2-4DB9-901B-7D86A74599DD};{94D48E72-5150-4D21-86E6-361506DAD28A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{94D48E72-5150-4D21-86E6-361506DAD28A};; -Siltojen korkeudet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7E8EA280-EAD4-4AFF-BA43-73DCA7805836};Avoin;{7E8EA280-EAD4-4AFF-BA43-73DCA7805836};{07943EAC-6AF9-4EB1-9B31-B47199446476};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{07943EAC-6AF9-4EB1-9B31-B47199446476};; -Soidensuojelun täydennysehdotus, Etelä-Suomi ja valtionmaan toteutuneet kohteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={5F14DCDF-6A2C-4A67-9035-BC73D0ED60B0};Avoin;{5F14DCDF-6A2C-4A67-9035-BC73D0ED60B0};{C0D331FF-68D8-4BD4-AEB3-B631AF824745};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C0D331FF-68D8-4BD4-AEB3-B631AF824745};; -Suokasvillisuusvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B6BD1CAD-BAB5-4473-AFD0-79DB30A88420};Avoin;{B6BD1CAD-BAB5-4473-AFD0-79DB30A88420};{193219CE-197F-4717-8D40-E7EDF6884A1E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{193219CE-197F-4717-8D40-E7EDF6884A1E};; -Tulvariskialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={83B57B72-1D1D-4DE4-BC61-180F3EE028AF};Avoin;{83B57B72-1D1D-4DE4-BC61-180F3EE028AF};{AF913A8D-484D-4313-ABD4-AE65A46A391E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AF913A8D-484D-4313-ABD4-AE65A46A391E};; -Tulvavaaravyöhykkeet, meritulva;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9EC02206-E14C-4186-AF09-829FFFB43FD9};Avoin;{9EC02206-E14C-4186-AF09-829FFFB43FD9};;;https://metadata.ymparisto.fi/dataset/;; -Tulvavaaravyöhykkeet, vesistötulva (avovesi);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C2B3FC73-060B-4053-BA04-54D86F4DE7E5};Avoin;{C2B3FC73-060B-4053-BA04-54D86F4DE7E5};;;https://metadata.ymparisto.fi/dataset/;; -Tunturialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A9B5E4AF-21B3-4FA5-AF45-2CD819CE742E};Avoin;{A9B5E4AF-21B3-4FA5-AF45-2CD819CE742E};{ADEAFCBE-9E2D-403A-8E78-13FE913D5E42};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{ADEAFCBE-9E2D-403A-8E78-13FE913D5E42};; -Uimavesidirektiivin mukaiset uimavedet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={918099FC-918C-4FA3-9444-5A3D5E950877};Avoin;{918099FC-918C-4FA3-9444-5A3D5E950877};{11F3E166-1B35-4628-BE18-45D1A09A097F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{11F3E166-1B35-4628-BE18-45D1A09A097F};; -Valtakunnallisesti arvokkaat kallioalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4CFB1D5B-F47E-4E92-918A-63EC5BB9C217};Avoin;{4CFB1D5B-F47E-4E92-918A-63EC5BB9C217};{626BAE8B-6632-43FD-8B3A-CF1E01662331};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{626BAE8B-6632-43FD-8B3A-CF1E01662331};; -Valtakunnallisesti arvokkaat moreenimuodostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A21026BF-204C-42FC-9FCB-450BD5A94C81};Avoin;{A21026BF-204C-42FC-9FCB-450BD5A94C81};{8253487E-FA94-4E83-9A85-71F85D4DA0B0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8253487E-FA94-4E83-9A85-71F85D4DA0B0};; -Valtakunnallisesti arvokkaat tuuli- ja rantakerrostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={66A5653E-A8E7-4BA3-9703-A0AF001AE089};Avoin;{66A5653E-A8E7-4BA3-9703-A0AF001AE089};{3298587D-C391-47C6-A4E0-D599D0206B73};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3298587D-C391-47C6-A4E0-D599D0206B73};; -Valuma-aluejako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={343C40A6-D0ED-4B6A-BA77-2724A48A3BD3};Avoin;{343C40A6-D0ED-4B6A-BA77-2724A48A3BD3};{44394B13-85D7-4998-BD06-8ADC77C7455C};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{44394B13-85D7-4998-BD06-8ADC77C7455C};; -Water turbidity daily product for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={89F4653E-9930-4540-BFBA-C78444E1FCD5};Avoin;{89F4653E-9930-4540-BFBA-C78444E1FCD5};{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};Finnish Environment Intitute (SYKE);https://metadata.ymparisto.fi/dataset/{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};; -Water turbidity daily product for the Baltic Sea 2012-2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B4783610-3298-4550-9793-9DBDCB0080B1};Avoin;{B4783610-3298-4550-9793-9DBDCB0080B1};;;https://metadata.ymparisto.fi/dataset/;; -Water turbidity weekly composite for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CEF2EA78-0400-46F1-8173-CD1D1EE547D1};Avoin;{CEF2EA78-0400-46F1-8173-CD1D1EE547D1};{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};;https://metadata.ymparisto.fi/dataset/{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};; -Vesienhoitoalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DFA8CF50-721B-4C71-93DD-A8264C5C2A9C};Avoin;{DFA8CF50-721B-4C71-93DD-A8264C5C2A9C};{6AF42DE4-EB9A-4568-9E6C-90BEF19673D0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6AF42DE4-EB9A-4568-9E6C-90BEF19673D0};; -Vesipuitedirektiivin mukaiset vesimuodostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CB8FEF32-5FCE-4E33-A8B8-1FBB02F9C2C1};Avoin;{CB8FEF32-5FCE-4E33-A8B8-1FBB02F9C2C1};{A3748B80-6E63-4460-A8B7-72CB9884F69B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A3748B80-6E63-4460-A8B7-72CB9884F69B};; -Yhdyskuntarakenteen vyöhykkeet 2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={96F338EA-75AF-432C-A780-31A3CDECBDF2};Avoin;{96F338EA-75AF-432C-A780-31A3CDECBDF2};{3B8BD61A-C137-46ED-B16A-B8BA27D1561B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3B8BD61A-C137-46ED-B16A-B8BA27D1561B};; -YKR Kaupunkiseudut;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={29205135-177D-4244-ABF3-BEE74782E567};Avoin;{29205135-177D-4244-ABF3-BEE74782E567};{E3E29357-4333-4C11-8E0E-FCD7307F91EE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E3E29357-4333-4C11-8E0E-FCD7307F91EE};; -YKR-aluejaot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={802F4259-8AB3-473A-810C-FA3BE4FE5C92};Avoin;{802F4259-8AB3-473A-810C-FA3BE4FE5C92};{A2CEE8A2-F188-4787-88FC-A393032CA60A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A2CEE8A2-F188-4787-88FC-A393032CA60A};; -YKR-taajama;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CB02E608-6E6B-417E-87DA-BD7C8CB53712};Avoin;{CB02E608-6E6B-417E-87DA-BD7C8CB53712};{28F79998-B5E2-4085-AF80-FB3FA5E9F9BA};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{28F79998-B5E2-4085-AF80-FB3FA5E9F9BA};; -Ympäristömeludirektiivin mukaiset melualueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9D872FA-3A6D-45C6-AD52-78E0724307A7};Avoin;{E9D872FA-3A6D-45C6-AD52-78E0724307A7};;;https://metadata.ymparisto.fi/dataset/;; -Ympäristömeludirektiivin mukaiset melualueet 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8B962E44-AB11-4D38-B178-76AB85B114BB};Avoin;{8B962E44-AB11-4D38-B178-76AB85B114BB};{FCF0230E-07C1-48FF-A762-B51F1332F1DC};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{FCF0230E-07C1-48FF-A762-B51F1332F1DC};; -Aluesadannan ja lumen aluevesiarvon laskenta-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9CC3C6F8-D834-4663-8424-D846CEE43F3D};Saatavuutta rajoitettu;{9CC3C6F8-D834-4663-8424-D846CEE43F3D};{248E7E3F-02A6-41AF-A977-5A4949966001};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{248E7E3F-02A6-41AF-A977-5A4949966001};; -ARA-rakennukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E97AFFB-BFDB-4077-BEDD-BE5A7D971C8E};Saatavuutta rajoitettu;{1E97AFFB-BFDB-4077-BEDD-BE5A7D971C8E};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Areas Included in National Conservation Programmes;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A0C487D4-968B-4553-9EFB-870D6D2A728C};Saatavuutta rajoitettu;{A0C487D4-968B-4553-9EFB-870D6D2A728C};;;https://metadata.ymparisto.fi/dataset/;; -Areas with Local Detailed Plans;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E60A3152-F0AA-4E61-BA1C-E28F0DBE8FF6};Saatavuutta rajoitettu;{E60A3152-F0AA-4E61-BA1C-E28F0DBE8FF6};;;https://metadata.ymparisto.fi/dataset/;; -Asemakaavoitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AC7E17C3-0833-40B1-B37A-187320A03826};Saatavuutta rajoitettu;{AC7E17C3-0833-40B1-B37A-187320A03826};{31968F40-0083-413A-B474-A6D2DB081EDF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{31968F40-0083-413A-B474-A6D2DB081EDF};; -Asemakaavojen seurantalomakkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9BCE41E0-54BC-43A4-9543-D3948FD151EF};Saatavuutta rajoitettu;{9BCE41E0-54BC-43A4-9543-D3948FD151EF};{9BCE41E0-54BC-43A4-9543-D3948FD151EF};Suomen ympäristökeskus / Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{9BCE41E0-54BC-43A4-9543-D3948FD151EF};; -Asianhallintajärjestelmä AHJO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DBBDEECB-AB2A-4B01-B45B-DE809992F79F};Saatavuutta rajoitettu;{DBBDEECB-AB2A-4B01-B45B-DE809992F79F};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Asuinrakennusten tiheys (rakennusta/km2);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={44F16A49-3DC8-4BB5-9420-6D015FB64EF1};Saatavuutta rajoitettu;{44F16A49-3DC8-4BB5-9420-6D015FB64EF1};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -AVOIMUUSINDEKSI MERENPOHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0189B2FD-4411-4CB0-90E5-DF4E1293C6F3};Saatavuutta rajoitettu;{0189B2FD-4411-4CB0-90E5-DF4E1293C6F3};{EB99F934-DCF8-4383-BD38-73B2DF3F0C72};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{EB99F934-DCF8-4383-BD38-73B2DF3F0C72};; -Bathing Waters;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1D154349-AFCA-4FBB-AD07-50EB6F36339D};Saatavuutta rajoitettu;{1D154349-AFCA-4FBB-AD07-50EB6F36339D};;;https://metadata.ymparisto.fi/dataset/;; -BCU-konttipaikat (öljyyntyneiden lintujen hoitoon);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EFC9B874-90BF-4701-BABF-29F6F7D58BE0};Saatavuutta rajoitettu;{EFC9B874-90BF-4701-BABF-29F6F7D58BE0};{DF0A5826-F1DF-42EB-A7A8-B8510A46B675};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{DF0A5826-F1DF-42EB-A7A8-B8510A46B675};; -BORIS 2 (Baltic Oil Response Information System);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={180ACCFE-FE84-4CD3-AF8A-6FF75C473059};Saatavuutta rajoitettu;{180ACCFE-FE84-4CD3-AF8A-6FF75C473059};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Catchment areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D6C6858A-562D-4965-AD77-2B1E97EFFA6B};Saatavuutta rajoitettu;{D6C6858A-562D-4965-AD77-2B1E97EFFA6B};;;https://metadata.ymparisto.fi/dataset/;; -CERAMIUM TENUICORNE LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D30F72F9-01F0-448A-9F18-213954BF87C7};Saatavuutta rajoitettu;{D30F72F9-01F0-448A-9F18-213954BF87C7};{F274E613-65AB-4867-B2C6-925596B7A8DE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F274E613-65AB-4867-B2C6-925596B7A8DE};; -CHARA ASPERA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F271AB26-3268-461F-A380-F3A1CC6AB62E};Saatavuutta rajoitettu;{F271AB26-3268-461F-A380-F3A1CC6AB62E};{BF849316-273C-4347-B151-D4F06460C314};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF849316-273C-4347-B151-D4F06460C314};; -CHARA SPP. & NITELLA SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={31CF9F00-FAA6-4158-B2E9-9D443776C4B1};Saatavuutta rajoitettu;{31CF9F00-FAA6-4158-B2E9-9D443776C4B1};;;https://metadata.ymparisto.fi/dataset/;; -CHARA TOMENTOSA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3895111B-8E40-4EC1-9256-B4C59B460832};Saatavuutta rajoitettu;{3895111B-8E40-4EC1-9256-B4C59B460832};{0408513A-8CCF-422F-A47B-F12F0946D6EA};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0408513A-8CCF-422F-A47B-F12F0946D6EA};; -CHORDA FILUM LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7D430B8-AEA0-4701-BD8F-533892EA0AD4};Saatavuutta rajoitettu;{B7D430B8-AEA0-4701-BD8F-533892EA0AD4};{CE550B14-5568-4C84-A67C-1473FC33E71F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CE550B14-5568-4C84-A67C-1473FC33E71F};; -CLADOPHORA GLOMERATA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={609B6F9F-954E-4F98-BCFC-172689200290};Saatavuutta rajoitettu;{609B6F9F-954E-4F98-BCFC-172689200290};{411FD599-AAC9-491D-95CA-BC942AE2FF72};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{411FD599-AAC9-491D-95CA-BC942AE2FF72};; -CLADOPHORA RUPESTRIS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0B5AAC11-E3E2-40D3-800F-8F5801F503AB};Saatavuutta rajoitettu;{0B5AAC11-E3E2-40D3-800F-8F5801F503AB};{37D789D3-A7E2-44FA-B227-58B59F962244};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{37D789D3-A7E2-44FA-B227-58B59F962244};; -Corine Land Cover 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EEC35D81-765A-4A7B-BC9B-F89A476807EC};Saatavuutta rajoitettu;{EEC35D81-765A-4A7B-BC9B-F89A476807EC};;;https://metadata.ymparisto.fi/dataset/;; -Corine Land Cover 2006;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DD648E4F-0478-4D32-8286-C2437EC87D33};Saatavuutta rajoitettu;{DD648E4F-0478-4D32-8286-C2437EC87D33};;;https://metadata.ymparisto.fi/dataset/;; -Corine Land Cover 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A58329D8-3ED0-47B8-B789-3095EE55C255};Saatavuutta rajoitettu;{A58329D8-3ED0-47B8-B789-3095EE55C255};;;https://metadata.ymparisto.fi/dataset/;; -Elinympäristön tietopalvelu Liiteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={629AC114-8BBC-48B5-9D8F-303A53CF3E9F};Saatavuutta rajoitettu;{629AC114-8BBC-48B5-9D8F-303A53CF3E9F};{1893C260-D934-48F8-A0A0-3606FC392438};Suomen ympäristökeskus / Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{1893C260-D934-48F8-A0A0-3606FC392438};; -Eliölajit-tietojärjestelmä TAXON;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C420898F-600A-41CB-8843-F06A90301427};Saatavuutta rajoitettu;{C420898F-600A-41CB-8843-F06A90301427};{C420898F-600A-41CB-8843-F06A90301427};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C420898F-600A-41CB-8843-F06A90301427};; -Energiantuotanto rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9707160B-8B54-48DB-9151-4A41FD46D159};Saatavuutta rajoitettu;{9707160B-8B54-48DB-9151-4A41FD46D159};{DBEAADBD-3EE0-498D-8EC2-C66191DA559C};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{DBEAADBD-3EE0-498D-8EC2-C66191DA559C};; -Environmental Noise Maps;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0CF94C00-D59A-45A0-A2EA-4ACF9014A2BA};Saatavuutta rajoitettu;{0CF94C00-D59A-45A0-A2EA-4ACF9014A2BA};;;https://metadata.ymparisto.fi/dataset/;; -EPHYDATIA FLUVIATILIS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB5AE7E2-8C84-4FCC-83DD-1150F60EEE27};Saatavuutta rajoitettu;{BB5AE7E2-8C84-4FCC-83DD-1150F60EEE27};{20637AA7-97D7-43AB-AF34-636043F1C1B9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{20637AA7-97D7-43AB-AF34-636043F1C1B9};; -Etäisyysvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={64764F78-0F35-438B-9EA4-FDAC3384840D};Saatavuutta rajoitettu;{64764F78-0F35-438B-9EA4-FDAC3384840D};{BF51D501-58F4-4932-A951-5B5A88F4D3C6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF51D501-58F4-4932-A951-5B5A88F4D3C6};; -Flood hazard areas, lake and river floods;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={302A9608-99B9-4D10-A469-36949D12436F};Saatavuutta rajoitettu;{302A9608-99B9-4D10-A469-36949D12436F};;;https://metadata.ymparisto.fi/dataset/;; -Flood hazard areas, sea floods;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6C21A825-FE70-47CE-91CA-E14EBE01F562};Saatavuutta rajoitettu;{6C21A825-FE70-47CE-91CA-E14EBE01F562};;;https://metadata.ymparisto.fi/dataset/;; -Flood Risk Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={13972098-7333-4827-B5D0-CFEF3CE54295};Saatavuutta rajoitettu;{13972098-7333-4827-B5D0-CFEF3CE54295};;;https://metadata.ymparisto.fi/dataset/;; -FONTINALIS SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={83CE79F8-4DF5-4B8A-B431-77FAC6195603};Saatavuutta rajoitettu;{83CE79F8-4DF5-4B8A-B431-77FAC6195603};{6EF6BCA9-D30B-45C1-8111-641DC55FA095};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6EF6BCA9-D30B-45C1-8111-641DC55FA095};; -Forest Vegetation Zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D53E9674-A446-4F54-81C3-8C15310353CC};Saatavuutta rajoitettu;{D53E9674-A446-4F54-81C3-8C15310353CC};;;https://metadata.ymparisto.fi/dataset/;; -FUCUS SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FB02515B-D7A9-4F6D-BD15-2E2B818AD997};Saatavuutta rajoitettu;{FB02515B-D7A9-4F6D-BD15-2E2B818AD997};{B6B48CE3-B9FA-442A-9FF5-F8E0B396578D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{B6B48CE3-B9FA-442A-9FF5-F8E0B396578D};; -FUCUS-YHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={76879C3D-8293-4C12-B873-26F59BCF54B9};Saatavuutta rajoitettu;{76879C3D-8293-4C12-B873-26F59BCF54B9};;;https://metadata.ymparisto.fi/dataset/;; -GOFREP-alue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B9487289-0C02-48E4-A26F-DAACDA60C462};Saatavuutta rajoitettu;{B9487289-0C02-48E4-A26F-DAACDA60C462};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Groundwater areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FA1742D1-8509-437C-846A-6637B3FF7345};Saatavuutta rajoitettu;{FA1742D1-8509-437C-846A-6637B3FF7345};;;https://metadata.ymparisto.fi/dataset/;; -HAURA- JA HAPSIKKAPOHJAT LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FF12F76-3162-4DD9-8320-668EDB9CBD9E};Saatavuutta rajoitettu;{6FF12F76-3162-4DD9-8320-668EDB9CBD9E};{C422872A-BA11-406C-9263-44D3302A7FCD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C422872A-BA11-406C-9263-44D3302A7FCD};; -Helsingin ihmispaineindeksi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={43163BBB-0D01-4375-BE28-33AA1607B2C3};Saatavuutta rajoitettu;{43163BBB-0D01-4375-BE28-33AA1607B2C3};{2063FB74-4879-4213-B9B5-E33FC848315E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2063FB74-4879-4213-B9B5-E33FC848315E};; -Historic Flood Maps;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FF5E8D4-5329-44D1-8741-D7F3223D0F30};Saatavuutta rajoitettu;{6FF5E8D4-5329-44D1-8741-D7F3223D0F30};;;https://metadata.ymparisto.fi/dataset/;; -HUB-biotooppiluokittelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7DB140D8-F133-42DB-B424-3309C2684A6D};Saatavuutta rajoitettu;{7DB140D8-F133-42DB-B424-3309C2684A6D};{E0001193-53A2-4CA3-B849-753621E51A69};Metsähallitus / Matti Sahla;https://metadata.ymparisto.fi/dataset/{E0001193-53A2-4CA3-B849-753621E51A69};; -Hydrologian ja vesien käytön tietojärjestelmä HYDRO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={86FC3188-6796-4C79-AC58-8DBC7B568827};Saatavuutta rajoitettu;{86FC3188-6796-4C79-AC58-8DBC7B568827};{86FC3188-6796-4C79-AC58-8DBC7B568827};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{86FC3188-6796-4C79-AC58-8DBC7B568827};; -Hätäkeskukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={51C9D60D-6D41-44BD-9136-C4933510DB2D};Saatavuutta rajoitettu;{51C9D60D-6D41-44BD-9136-C4933510DB2D};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Ilmapäästötietojärjestelmä IPTJ;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={033E574E-3B6D-4A40-A2BC-8C50C8FA86E0};Saatavuutta rajoitettu;{033E574E-3B6D-4A40-A2BC-8C50C8FA86E0};;;https://metadata.ymparisto.fi/dataset/;; -Jokisuistot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6DEF8642-7867-4047-AEB9-5134D24C4D64};Saatavuutta rajoitettu;{6DEF8642-7867-4047-AEB9-5134D24C4D64};{5B7F87B2-F082-4081-B653-3AEF12446C9E};Metsähallitus;https://metadata.ymparisto.fi/dataset/{5B7F87B2-F082-4081-B653-3AEF12446C9E};; -Järvirekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E4505311-A27A-4CD5-B207-89EFEEFE48CF};Saatavuutta rajoitettu;{E4505311-A27A-4CD5-B207-89EFEEFE48CF};{E4505311-A27A-4CD5-B207-89EFEEFE48CF};Suomen ympäristökeskus /Vesikeskus;https://metadata.ymparisto.fi/dataset/{E4505311-A27A-4CD5-B207-89EFEEFE48CF};; -Järvirekisteri / Säännöstelyhankkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};Saatavuutta rajoitettu;{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};; -Jätevedenpuhdistamot rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CE26C069-1F54-4F78-8FE2-A1C025E8C9E8};Saatavuutta rajoitettu;{CE26C069-1F54-4F78-8FE2-A1C025E8C9E8};{E1663280-9639-4559-B0EA-18D1B4BD5625};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{E1663280-9639-4559-B0EA-18D1B4BD5625};; -Kaavoituksen seurannan tilasto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4A29A502-D287-4956-AD84-8C467D1E4D40};Saatavuutta rajoitettu;{4A29A502-D287-4956-AD84-8C467D1E4D40};{4A29A502-D287-4956-AD84-8C467D1E4D40};Suomen ympäristökeskus/Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{4A29A502-D287-4956-AD84-8C467D1E4D40};; -Kalankasvatus ja verkkoaltaat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F4A6DADE-650B-4BA2-8E69-11ED37EC1335};Saatavuutta rajoitettu;{F4A6DADE-650B-4BA2-8E69-11ED37EC1335};{D2396FA4-DFF0-4D88-A647-70256C18941E};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{D2396FA4-DFF0-4D88-A647-70256C18941E};; -Kansainväliset jätesiirrot -rekisteri (Basel-rekisteri);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8D42F658-9DAD-4C65-9418-ABB00B92DF98};Saatavuutta rajoitettu;{8D42F658-9DAD-4C65-9418-ABB00B92DF98};{8D42F658-9DAD-4C65-9418-ABB00B92DF98};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8D42F658-9DAD-4C65-9418-ABB00B92DF98};; -Kansalliset kaupunkipuistot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={ECE5D7C1-FF3C-47C2-84F1-D878CD3DA6EE};Saatavuutta rajoitettu;{ECE5D7C1-FF3C-47C2-84F1-D878CD3DA6EE};{43E99E3C-7C91-42F0-A324-C9CB086E65DD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{43E99E3C-7C91-42F0-A324-C9CB086E65DD};; -Kasviplanktontietojärjestelmä KPLANK;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2F164F22-A4F9-468A-97E5-717B0B3D0E7B};Saatavuutta rajoitettu;{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};;https://metadata.ymparisto.fi/dataset/{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};; -Kemikaalien ympäristötietorekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={04B79E94-8D3B-451E-963B-19DDAD6F6215};Saatavuutta rajoitettu;{04B79E94-8D3B-451E-963B-19DDAD6F6215};;;https://metadata.ymparisto.fi/dataset/;; -Kertymärekisteri KERTY;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FF5FE8B1-B2BC-44C7-8A47-744F0A522CD2};Saatavuutta rajoitettu;{FF5FE8B1-B2BC-44C7-8A47-744F0A522CD2};;;https://metadata.ymparisto.fi/dataset/;; -Koekalastusrekisteri (Nordic-verkot, Coastal-verkot, sähkökoekalastus);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={348DFB97-5F9F-4902-9152-23B0C9505673};Saatavuutta rajoitettu;{348DFB97-5F9F-4902-9152-23B0C9505673};;Luonnonvarakeskus;https://metadata.ymparisto.fi/dataset/;; -Kuntien poikkeamispäätökset ja suunnittelutarveratkaisut sekä ELY-keskusten tekemät poikkeamispäätökset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0A81369B-6693-4BEB-80EF-238035480C3C};Saatavuutta rajoitettu;{0A81369B-6693-4BEB-80EF-238035480C3C};;;https://metadata.ymparisto.fi/dataset/;; -Laboratorion ja näytteenoton tiedonhallintajärjestelmä LIMS;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={939AE4E5-332A-4B0C-B9BF-96E0F16620A0};Saatavuutta rajoitettu;{939AE4E5-332A-4B0C-B9BF-96E0F16620A0};;;https://metadata.ymparisto.fi/dataset/;; -Lake and River Depth Profiles;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={23FB0660-5B2B-4212-BE02-772EB6E27E16};Saatavuutta rajoitettu;{23FB0660-5B2B-4212-BE02-772EB6E27E16};;;https://metadata.ymparisto.fi/dataset/;; -Laskeumarekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={19002DDF-91D0-48A3-8E68-891C8E7374D5};Saatavuutta rajoitettu;{19002DDF-91D0-48A3-8E68-891C8E7374D5};;;https://metadata.ymparisto.fi/dataset/;; -Liikennealueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F47A597F-FC14-47C5-BD3A-AE0710BC8A08};Saatavuutta rajoitettu;{F47A597F-FC14-47C5-BD3A-AE0710BC8A08};{22C3E0C5-3221-408E-B346-60493CCE6361};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{22C3E0C5-3221-408E-B346-60493CCE6361};; -Lumen peittämä ala Itämeren alueella 2007-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=Metadata_SYKE_SCA_Baltic_0.05deg_pv;Saatavuutta rajoitettu;Metadata_SYKE_SCA_Baltic_0.05deg_pv;;;https://metadata.ymparisto.fi/dataset/;; -Lumen peittämä ala Suomessa 2006-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=Metadata_SYKE_SCA_2006-2011_Finland;Saatavuutta rajoitettu;Metadata_SYKE_SCA_2006-2011_Finland;;;https://metadata.ymparisto.fi/dataset/;; -Maa-ainesluvat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={604DFF56-4157-46DE-84CB-71978DAC790A};Saatavuutta rajoitettu;{604DFF56-4157-46DE-84CB-71978DAC790A};{F163EC63-72A8-4FB5-A021-7A828BC1923F};KEHA-keskus;https://metadata.ymparisto.fi/dataset/{F163EC63-72A8-4FB5-A021-7A828BC1923F};; -Maaperän tilan tietojärjestelmä MATTI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};Saatavuutta rajoitettu;{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};Suomen ympäristökeskus ja ELY-keskukset;https://metadata.ymparisto.fi/dataset/{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};; -Maastoliikennerajoitukset-karttapalvelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E8222F77-00C4-4934-B2CB-0AB6A93D9E69};Saatavuutta rajoitettu;{E8222F77-00C4-4934-B2CB-0AB6A93D9E69};{8F7C8F06-B945-4D95-91BE-F0ECF1D76B7E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8F7C8F06-B945-4D95-91BE-F0ECF1D76B7E};; -Mahdolliset kapeat murtovesilahdet (1650);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8770CC62-F274-4700-9A81-592FBDE66635};Saatavuutta rajoitettu;{8770CC62-F274-4700-9A81-592FBDE66635};;;https://metadata.ymparisto.fi/dataset/;; -Mahdolliset laajat matalat lahdet (1160);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6655C0FA-9562-4015-B97A-0D07A19CF72C};Saatavuutta rajoitettu;{6655C0FA-9562-4015-B97A-0D07A19CF72C};;;https://metadata.ymparisto.fi/dataset/;; -Mahdolliset rannikon laguunit (1150);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={74345ACE-9D96-4A52-9E2F-7E8FE38C7809};Saatavuutta rajoitettu;{74345ACE-9D96-4A52-9E2F-7E8FE38C7809};;;https://metadata.ymparisto.fi/dataset/;; -MERENPOHJAN KIVIKKOISUUDEN TODENNÄKÖISYYS;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={41482CFB-D636-4396-AAB3-CBC82A28772A};Saatavuutta rajoitettu;{41482CFB-D636-4396-AAB3-CBC82A28772A};;;https://metadata.ymparisto.fi/dataset/;; -Merialuejako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3F43EA83-48C3-4D04-9DDF-67822FB32EF2};Saatavuutta rajoitettu;{3F43EA83-48C3-4D04-9DDF-67822FB32EF2};{6030C360-861F-4C16-B4BB-6A0EB4B5CF96};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6030C360-861F-4C16-B4BB-6A0EB4B5CF96};; -Merihiekan- ja soranottoalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={46D1A89C-7CE9-420E-B592-4018AC17EEE8};Saatavuutta rajoitettu;{46D1A89C-7CE9-420E-B592-4018AC17EEE8};{E35DE705-2986-43F2-8678-F219F1D097F2};Metsähallitus;https://metadata.ymparisto.fi/dataset/{E35DE705-2986-43F2-8678-F219F1D097F2};; -Meripelastusvastuualue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A2C05340-3276-4FBB-8350-607560E3C31F};Saatavuutta rajoitettu;{A2C05340-3276-4FBB-8350-607560E3C31F};{22A9AD4A-E725-4426-BD1A-916A23CF73BE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{22A9AD4A-E725-4426-BD1A-916A23CF73BE};; -MERIVEDEN LÄMPÖTILA POHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={ED3D3E91-8674-4532-8E78-3E63950A9BE5};Saatavuutta rajoitettu;{ED3D3E91-8674-4532-8E78-3E63950A9BE5};{8E562E41-5465-40A1-9990-4EACE2537F01};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8E562E41-5465-40A1-9990-4EACE2537F01};; -Meriveden suolaisuus pinnalla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EA4227EC-6958-4EC1-AE3C-3051FB9678B8};Saatavuutta rajoitettu;{EA4227EC-6958-4EC1-AE3C-3051FB9678B8};{D9F7BA00-14B9-40A0-9FEB-A9549266D7FF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D9F7BA00-14B9-40A0-9FEB-A9549266D7FF};; -MERIVEDEN SUOLAISUUS POHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7E5A9C8-08A2-4272-9AB2-ADA81A2E6C69};Saatavuutta rajoitettu;{B7E5A9C8-08A2-4272-9AB2-ADA81A2E6C69};;;https://metadata.ymparisto.fi/dataset/;; -Mire vegetation zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={79F1A09C-54C6-4EBE-96D8-1970459896EE};Saatavuutta rajoitettu;{79F1A09C-54C6-4EBE-96D8-1970459896EE};;;https://metadata.ymparisto.fi/dataset/;; -MRL:n mukaisten yleiskaavojen ulkorajaukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D8DD3305-0313-47F4-B241-71658B6C06AC};Saatavuutta rajoitettu;{D8DD3305-0313-47F4-B241-71658B6C06AC};;;https://metadata.ymparisto.fi/dataset/;; -MYTILUS TROSSULUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FECA3DEC-6C77-4424-BC42-0AE1452B2BA0};Saatavuutta rajoitettu;{FECA3DEC-6C77-4424-BC42-0AE1452B2BA0};{6AC94879-B22D-42DA-B3F5-3FC955AE03E4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6AC94879-B22D-42DA-B3F5-3FC955AE03E4};; -NAJAS MARINA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6906D7D5-BAEC-4006-8FDB-84732B61ADE7};Saatavuutta rajoitettu;{6906D7D5-BAEC-4006-8FDB-84732B61ADE7};{61E89CB2-03BA-4FD5-A231-BA2D909D30EF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{61E89CB2-03BA-4FD5-A231-BA2D909D30EF};; -Nannut-luokittelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8A4E6487-B27A-4F3B-8EA4-4651EA39ACCE};Saatavuutta rajoitettu;{8A4E6487-B27A-4F3B-8EA4-4651EA39ACCE};{F60E6BF7-9BC6-476D-8102-2E310A204BE0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F60E6BF7-9BC6-476D-8102-2E310A204BE0};; -National Database of Regional Land Use Plans;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2FAA5D6B-C053-465E-812C-119798581F5C};Saatavuutta rajoitettu;{2FAA5D6B-C053-465E-812C-119798581F5C};;;https://metadata.ymparisto.fi/dataset/;; -National Landscape Division;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={09F5613F-D9A2-4C68-818D-70B018437888};Saatavuutta rajoitettu;{09F5613F-D9A2-4C68-818D-70B018437888};;;https://metadata.ymparisto.fi/dataset/;; -Nationally Designated Nature Protected Areas and Wilderness Reserves;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9871C541-9E84-4241-A1E5-51C596A5A4E2};Saatavuutta rajoitettu;{9871C541-9E84-4241-A1E5-51C596A5A4E2};;;https://metadata.ymparisto.fi/dataset/;; -Nationally Valuable Aeolian and Littoral Deposits;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={04399CD4-86CD-453E-B683-5CFF53FA36D0};Saatavuutta rajoitettu;{04399CD4-86CD-453E-B683-5CFF53FA36D0};;;https://metadata.ymparisto.fi/dataset/;; -Nationally Valuable Moraine Formations;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0193799E-1F36-4FB9-A0B9-48264860C58C};Saatavuutta rajoitettu;{0193799E-1F36-4FB9-A0B9-48264860C58C};;;https://metadata.ymparisto.fi/dataset/;; -Nationally Valuable Rocky Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2753EE72-1F06-4291-987F-08FA693ED5CA};Saatavuutta rajoitettu;{2753EE72-1F06-4291-987F-08FA693ED5CA};;;https://metadata.ymparisto.fi/dataset/;; -Natura 2000 -tietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7AC30A3E-8E38-4717-8D2E-80585623212B};Saatavuutta rajoitettu;{7AC30A3E-8E38-4717-8D2E-80585623212B};;Suomen ympäristökeskus / Luontoympäristökeskus / Biodiversiteettiyksikkö;https://metadata.ymparisto.fi/dataset/;; -Nature Recreational Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9F0B1FD-C41E-46A1-BA30-E9A29DF12D70};Saatavuutta rajoitettu;{E9F0B1FD-C41E-46A1-BA30-E9A29DF12D70};;;https://metadata.ymparisto.fi/dataset/;; -NÄKÖSYVYYS (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={55AB842F-9CED-4E80-A7E5-07A54F0AE4A4};Saatavuutta rajoitettu;{55AB842F-9CED-4E80-A7E5-07A54F0AE4A4};{811B434E-75A1-4F53-9445-A4B4BDADA499};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{811B434E-75A1-4F53-9445-A4B4BDADA499};; -Olemassa oleva ja suunniteltu tuulivoima;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9B856E2E-6F8F-4B9A-8618-9923F238852D};Saatavuutta rajoitettu;{9B856E2E-6F8F-4B9A-8618-9923F238852D};{F690781C-D187-4611-8D80-A703804F7B59};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F690781C-D187-4611-8D80-A703804F7B59};; -Oppilaitokset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7753FF3C-D1E8-453A-B3F1-8412A2AD1CD1};Saatavuutta rajoitettu;{7753FF3C-D1E8-453A-B3F1-8412A2AD1CD1};;Tilastokeskus;https://metadata.ymparisto.fi/dataset/;; -Outlines of Local Master Plans Ratified in Accordance to the old Building Act;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6771A0AD-3B9C-450E-9536-0B036B47A662};Saatavuutta rajoitettu;{6771A0AD-3B9C-450E-9536-0B036B47A662};;;https://metadata.ymparisto.fi/dataset/;; -Outlines of Local Master Plans Ratified in Accordance with the Landuse and Building Act;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2D70E79B-36F8-4E5E-BB6C-0DD7C6E5639D};Saatavuutta rajoitettu;{2D70E79B-36F8-4E5E-BB6C-0DD7C6E5639D};;;https://metadata.ymparisto.fi/dataset/;; -Paikkatietoaineistojen latauspalvelu LAPIO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DDFEA0F7-7A42-4E8E-9782-57DD74697BD1};Saatavuutta rajoitettu;{DDFEA0F7-7A42-4E8E-9782-57DD74697BD1};{3EEF491C-53D4-4614-B64C-1D98A699198C};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3EEF491C-53D4-4614-B64C-1D98A699198C};; -Paikkatietoanalyysien tuloksia;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BBDCFF54-8109-4795-8BB5-15FA9480BB9C};Saatavuutta rajoitettu;{BBDCFF54-8109-4795-8BB5-15FA9480BB9C};{F039C4B7-7D3C-4EFA-99B0-C52091BC97B4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F039C4B7-7D3C-4EFA-99B0-C52091BC97B4};; -Patoturvallisuuden tietojärjestelmä PATOTURVA;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={895902E7-031C-48AD-A8AC-DCE7D325B9EE};Saatavuutta rajoitettu;{895902E7-031C-48AD-A8AC-DCE7D325B9EE};;;https://metadata.ymparisto.fi/dataset/;; -Pelastustoimen alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={75182F9D-D7D5-4A3A-8556-1C96577F0A52};Saatavuutta rajoitettu;{75182F9D-D7D5-4A3A-8556-1C96577F0A52};{34A6DAEE-DE66-45A2-B3A1-CB23CF6FF45E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{34A6DAEE-DE66-45A2-B3A1-CB23CF6FF45E};; -PINNAN AALTOEKSPOSITIO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB655D7F-1E25-4E19-AE7F-5DEBF1AEFC6C};Saatavuutta rajoitettu;{BB655D7F-1E25-4E19-AE7F-5DEBF1AEFC6C};{216DD75B-59A8-408A-80CD-F0785E548769};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{216DD75B-59A8-408A-80CD-F0785E548769};; -Pintavesien tilan tietojärjestelmä, vedenlaatu PIVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B1444E19-0F36-49F5-A849-01A3D2083A11};Saatavuutta rajoitettu;{B1444E19-0F36-49F5-A849-01A3D2083A11};;;https://metadata.ymparisto.fi/dataset/;; -Pohjaeläinhavaintoja POHJE-tietokannasta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2FA13650-7CB3-4299-8663-7542769D102E};Saatavuutta rajoitettu;{2FA13650-7CB3-4299-8663-7542769D102E};{0A22C866-3D0C-476A-9957-9275EB18B8AD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0A22C866-3D0C-476A-9957-9275EB18B8AD};; -Pohjaeläintietojärjestelmä - POHJE;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};Saatavuutta rajoitettu;{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};;https://metadata.ymparisto.fi/dataset/{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};; -Pohjavesitietojärjestelmä POVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C594F22E-B642-434D-B6F2-6ECB03F760C7};Saatavuutta rajoitettu;{C594F22E-B642-434D-B6F2-6ECB03F760C7};{C594F22E-B642-434D-B6F2-6ECB03F760C7};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C594F22E-B642-434D-B6F2-6ECB03F760C7};; -Poronhoidon paikkatiedot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={12E7100A-C010-40A3-A441-4EED54C9FC64};Saatavuutta rajoitettu;{12E7100A-C010-40A3-A441-4EED54C9FC64};{A6E6BD51-E463-4E0A-88EC-71DBFDF2223E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A6E6BD51-E463-4E0A-88EC-71DBFDF2223E};; -POTAMOGETON PERFOLIATUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D7F3685D-0432-4FC3-B0D4-13E798050A67};Saatavuutta rajoitettu;{D7F3685D-0432-4FC3-B0D4-13E798050A67};{8FD8F68C-373B-4A47-BB76-2E9040F3C488};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8FD8F68C-373B-4A47-BB76-2E9040F3C488};; -Protected Rapids;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9540CC1E-3E98-4C3D-A214-DC26A7DE6953};Saatavuutta rajoitettu;{9540CC1E-3E98-4C3D-A214-DC26A7DE6953};;;https://metadata.ymparisto.fi/dataset/;; -PUNALEVÄYHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={179A08D0-3B37-4A80-837E-83F151D5CDEA};Saatavuutta rajoitettu;{179A08D0-3B37-4A80-837E-83F151D5CDEA};;;https://metadata.ymparisto.fi/dataset/;; -Puolustusvoimien suoja-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={61D3FA7B-D8AC-40BA-A552-9FCED8F87375};Saatavuutta rajoitettu;{61D3FA7B-D8AC-40BA-A552-9FCED8F87375};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -PYLAIELLA LITTORALIS & ECTOCARPUS SILICULOSUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CF4921DE-7CD1-48C6-8C9C-1BFFC6662E14};Saatavuutta rajoitettu;{CF4921DE-7CD1-48C6-8C9C-1BFFC6662E14};{955901B1-C6D0-45CA-8EA6-C1EF95CA1C50};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{955901B1-C6D0-45CA-8EA6-C1EF95CA1C50};; -Rakennusperinnön hoitoavustushakemukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9D206B64-2EB7-47CC-AC9C-48459242444E};Saatavuutta rajoitettu;{9D206B64-2EB7-47CC-AC9C-48459242444E};{2CAEA52F-82FA-4E96-8E79-1327F27EF9A9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2CAEA52F-82FA-4E96-8E79-1327F27EF9A9};; -Rakennusperinnön inventointi kohteet ja alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4B84256C-0F01-456E-8A41-63231FC0D8F5};Saatavuutta rajoitettu;{4B84256C-0F01-456E-8A41-63231FC0D8F5};{D653718F-EE09-4F08-95B5-2AC42BAD5337};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D653718F-EE09-4F08-95B5-2AC42BAD5337};; -Rantalohkojako (SÖKÖ-toimintamalli);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={11A39DCD-4A35-4B90-AFBD-EEC3B778378C};Saatavuutta rajoitettu;{11A39DCD-4A35-4B90-AFBD-EEC3B778378C};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -River Basin Districts;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={65C4C67D-94F6-4B52-A656-069CC5853778};Saatavuutta rajoitettu;{65C4C67D-94F6-4B52-A656-069CC5853778};;;https://metadata.ymparisto.fi/dataset/;; -Ruoppaukset rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1A50B09C-2DEF-46C0-8664-19A3A5BF89CE};Saatavuutta rajoitettu;{1A50B09C-2DEF-46C0-8664-19A3A5BF89CE};{911BBDBD-5371-44E5-8AEA-3DEE2248AEE6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{911BBDBD-5371-44E5-8AEA-3DEE2248AEE6};; -Saaristomeren ja Selkämeren fladat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={45E434FE-36DF-431C-A9F2-D65C433F8751};Saatavuutta rajoitettu;{45E434FE-36DF-431C-A9F2-D65C433F8751};{FC7C4F23-81E4-4037-9DED-E45575205D30};Turun ammattikorkeakoulu;https://metadata.ymparisto.fi/dataset/{FC7C4F23-81E4-4037-9DED-E45575205D30};; -Sea Region Division;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={313F5784-71A0-46C2-A5B2-86AD9B17A4E8};Saatavuutta rajoitettu;{313F5784-71A0-46C2-A5B2-86AD9B17A4E8};;;https://metadata.ymparisto.fi/dataset/;; -Shoreline10 (Ranta10) and River network;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CED8AC3C-0651-4117-88A6-44CD05F6B42D};Saatavuutta rajoitettu;{CED8AC3C-0651-4117-88A6-44CD05F6B42D};;;https://metadata.ymparisto.fi/dataset/;; -SINISIMPUKKAYHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2F7CC7D6-380E-4E77-834F-7682EC6BC4FF};Saatavuutta rajoitettu;{2F7CC7D6-380E-4E77-834F-7682EC6BC4FF};;;https://metadata.ymparisto.fi/dataset/;; -STUCKENIA FILIFORMIS & STUCKENIA PECTINATA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BD151356-6EDB-4CB2-9BEF-E72D5AEBE936};Saatavuutta rajoitettu;{BD151356-6EDB-4CB2-9BEF-E72D5AEBE936};{BC774445-3D76-4F4F-B6A4-A63C631F0BAE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BC774445-3D76-4F4F-B6A4-A63C631F0BAE};; -Sukelluksissa havaittujen makroleväsukujen otannalla korjattu määrä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FF890247-EDD4-45E2-97BE-4570039870B7};Saatavuutta rajoitettu;{FF890247-EDD4-45E2-97BE-4570039870B7};{FBE17630-F6E1-47EA-B07E-C4A29BC33E3D};Metsähallitus;https://metadata.ymparisto.fi/dataset/{FBE17630-F6E1-47EA-B07E-C4A29BC33E3D};; -SYKEn hakemistorajapinta (OData);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3563384E-DF21-49F6-9E0A-CCB1B8A74A44};Saatavuutta rajoitettu;{3563384E-DF21-49F6-9E0A-CCB1B8A74A44};{318D5E0E-7E13-47F2-93E7-04DFE2A12917};Suomen ympäristökeskus / Tietokeskus;https://metadata.ymparisto.fi/dataset/{318D5E0E-7E13-47F2-93E7-04DFE2A12917};; -Säähavaintoasemat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D2BF7AA2-6E0A-4EFF-BB77-1EA5330A3518};Saatavuutta rajoitettu;{D2BF7AA2-6E0A-4EFF-BB77-1EA5330A3518};{CDE7624A-AA3A-4592-B086-24008F05108D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CDE7624A-AA3A-4592-B086-24008F05108D};; -SÖKÖ-karttalehdet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3A7B5ECF-5A1A-4CE8-8880-EFA161B0452E};Saatavuutta rajoitettu;{3A7B5ECF-5A1A-4CE8-8880-EFA161B0452E};{D925EC77-C5F0-4D1E-A691-93ED0FACDAD6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D925EC77-C5F0-4D1E-A691-93ED0FACDAD6};; -Teollisuus rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BDBE0944-5B2C-4CE8-A404-34B8292DF808};Saatavuutta rajoitettu;{BDBE0944-5B2C-4CE8-A404-34B8292DF808};{8D033A6C-D9A7-4622-B518-E4358303BD2C};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{8D033A6C-D9A7-4622-B518-E4358303BD2C};; -Tieriskirekisteri TSRR;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8047A089-C13F-4729-920B-3C18F471F711};Saatavuutta rajoitettu;{8047A089-C13F-4729-920B-3C18F471F711};{47A9B147-CC81-460F-976B-E3A2A756D15A};Pohjavesiaineisto- SYKE, Tieaineisto- LiV;https://metadata.ymparisto.fi/dataset/{47A9B147-CC81-460F-976B-E3A2A756D15A};; -Total Suspended Matter, FI-LAKES, 2012-06-20;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=e98cabc7-65ad-4e72-a6d6-f42e2cc591da;Saatavuutta rajoitettu;e98cabc7-65ad-4e72-a6d6-f42e2cc591da;;;https://metadata.ymparisto.fi/dataset/;; -Total suspended matter, FI-SOUTH, 2009-09-15;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=69b7df80-3d67-11e2-941e-005056c00008;Saatavuutta rajoitettu;69b7df80-3d67-11e2-941e-005056c00008;;;https://metadata.ymparisto.fi/dataset/;; -Traffic and Water Ways' Restriction Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A8126E3C-8F96-43C3-883D-BF315E38284E};Saatavuutta rajoitettu;{A8126E3C-8F96-43C3-883D-BF315E38284E};;;https://metadata.ymparisto.fi/dataset/;; -Traffic Risk Zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F1E7CB96-6CA3-4E51-B057-464AF658C46E};Saatavuutta rajoitettu;{F1E7CB96-6CA3-4E51-B057-464AF658C46E};;;https://metadata.ymparisto.fi/dataset/;; -Tulvariskiruudut;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7F0ABF2F-A175-4D9A-A04B-C6514B94D082};Saatavuutta rajoitettu;{7F0ABF2F-A175-4D9A-A04B-C6514B94D082};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; -Tulvatietojärjestelmä TULVATJ;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={58AAFEFC-1429-4343-8EE2-F5E136E0A276};Saatavuutta rajoitettu;{58AAFEFC-1429-4343-8EE2-F5E136E0A276};{58AAFEFC-1429-4343-8EE2-F5E136E0A276};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{58AAFEFC-1429-4343-8EE2-F5E136E0A276};; -Tuottajavastuutietojärjestelmä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0E73BB28-1574-48A0-87CA-FD4BC1211896};Saatavuutta rajoitettu;{0E73BB28-1574-48A0-87CA-FD4BC1211896};;;https://metadata.ymparisto.fi/dataset/;; -Uomatietojärjestelmä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FB936A19-421D-486D-95AC-009BD53AA1B2};Saatavuutta rajoitettu;{FB936A19-421D-486D-95AC-009BD53AA1B2};{FB936A19-421D-486D-95AC-009BD53AA1B2};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{FB936A19-421D-486D-95AC-009BD53AA1B2};; -Valtakunnallinen maakuntakaavapaikkatietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8E578155-1FD3-4B69-B6EB-0AB18B8F6FE6};Saatavuutta rajoitettu;{8E578155-1FD3-4B69-B6EB-0AB18B8F6FE6};{82174E6B-DBC6-439F-A0F0-B425C8FBB28F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{82174E6B-DBC6-439F-A0F0-B425C8FBB28F};; -Valtakunnallisen yöperhosseurannan keskustietokanta YÖPETI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1FF762A2-EE3F-415C-9895-D866B2E8A9CF};Saatavuutta rajoitettu;{1FF762A2-EE3F-415C-9895-D866B2E8A9CF};{4CA7BB48-01F0-4544-B153-E17569155FBD};SYKE;https://metadata.ymparisto.fi/dataset/{4CA7BB48-01F0-4544-B153-E17569155FBD};; -Valtion öljyntorjuntavarastot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3E35CBD1-1C57-4E17-96EC-6E41256E4619};Saatavuutta rajoitettu;{3E35CBD1-1C57-4E17-96EC-6E41256E4619};{872D9B86-1902-4343-999C-2F76348C4524};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{872D9B86-1902-4343-999C-2F76348C4524};; -Value-valuma-aluejako (ehdotus);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={31B11BD6-7886-447C-8A24-2B8FFE6B2D07};Saatavuutta rajoitettu;{31B11BD6-7886-447C-8A24-2B8FFE6B2D07};{E848DF66-C3A7-47E5-956A-5FEBBDBD924B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E848DF66-C3A7-47E5-956A-5FEBBDBD924B};; -Valvonta ja kuormitustietojärjestelmä VAHTI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D1539BE1-7A48-4CC8-AEC9-B32C7650A399};Saatavuutta rajoitettu;{D1539BE1-7A48-4CC8-AEC9-B32C7650A399};;;https://metadata.ymparisto.fi/dataset/;; -Vanhan rakennuslain mukaisesti vahvistettujen yleiskaavojen rasterit;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1377662D-F6F1-48F7-86A0-ADD001F08CE1};Saatavuutta rajoitettu;{1377662D-F6F1-48F7-86A0-ADD001F08CE1};{970599E7-8793-47A6-BF67-23E845E82ED1};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{970599E7-8793-47A6-BF67-23E845E82ED1};; -Vanhan rakennuslain mukaisesti vahvistettujen yleiskaavojen ulkorajaukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E200F2D-7983-42C1-84AF-459FD31AB4EC};Saatavuutta rajoitettu;{1E200F2D-7983-42C1-84AF-459FD31AB4EC};{AAFD1646-16FC-42D8-9F1B-DA8272A9D062};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AAFD1646-16FC-42D8-9F1B-DA8272A9D062};; -Varsinais-Suomen veneluiskat 2016;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AFB2C26E-E63B-4324-8E06-3B806EC37C92};Saatavuutta rajoitettu;{AFB2C26E-E63B-4324-8E06-3B806EC37C92};{5E45B067-25E6-41F2-8129-DD7C2BF67148};Turun ammattikorkeakoulu Oy;https://metadata.ymparisto.fi/dataset/{5E45B067-25E6-41F2-8129-DD7C2BF67148};; -Varsinais-Suomen väylien riskikartoitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={00DEEA25-2286-49B4-AED4-CB20A1BA5B9E};Saatavuutta rajoitettu;{00DEEA25-2286-49B4-AED4-CB20A1BA5B9E};{AB829531-5139-4A9D-9CF6-162AE62B27D0};Turun ammattikorkeakoulu;https://metadata.ymparisto.fi/dataset/{AB829531-5139-4A9D-9CF6-162AE62B27D0};; -VELMU kartoitusmenetelmät;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7F62E2AC-7C1B-4B3F-A66A-8BE03E01388B};Saatavuutta rajoitettu;{7F62E2AC-7C1B-4B3F-A66A-8BE03E01388B};{2BED2EB3-CDAA-4071-ADD3-1FC081C23440};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2BED2EB3-CDAA-4071-ADD3-1FC081C23440};; -VELMU kuvapisteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FBE41DF9-F410-4490-B9B6-8418CB33617E};Saatavuutta rajoitettu;{FBE41DF9-F410-4490-B9B6-8418CB33617E};{4504165A-74A4-4675-B95A-FA5274C4984A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{4504165A-74A4-4675-B95A-FA5274C4984A};; -VELMU lajihavainnot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D03358DD-CA34-487F-8F85-03A74C9D7296};Saatavuutta rajoitettu;{D03358DD-CA34-487F-8F85-03A74C9D7296};{C3C7C925-5872-4C30-A824-8F723BE3ABC8};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C3C7C925-5872-4C30-A824-8F723BE3ABC8};; -VELMU lajihavainnot (uhanalaiset lajit);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F500A951-A177-4CAA-83C5-FC54F2255FAB};Saatavuutta rajoitettu;{F500A951-A177-4CAA-83C5-FC54F2255FAB};{BF30DFF5-B5F8-4D8D-BAF7-D3671BBE432A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF30DFF5-B5F8-4D8D-BAF7-D3671BBE432A};; -Velmu taustakartta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B2B2A595-1225-4E18-B49C-EEBB5A6C5CE3};Saatavuutta rajoitettu;{B2B2A595-1225-4E18-B49C-EEBB5A6C5CE3};{60776441-5F9D-43EB-B902-A30CFBE89C2A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{60776441-5F9D-43EB-B902-A30CFBE89C2A};; -Venesatamat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4166A97E-E513-4B69-B498-447A8FA71B9A};Saatavuutta rajoitettu;{4166A97E-E513-4B69-B498-447A8FA71B9A};{D4C89F91-AA18-4359-B1D6-42C9C436332A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D4C89F91-AA18-4359-B1D6-42C9C436332A};; -Vesienhoitoalueiden pintavesien kaikki seurantapaikat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={356F0345-ABFE-4245-8231-BE4BFA83C541};Saatavuutta rajoitettu;{356F0345-ABFE-4245-8231-BE4BFA83C541};{79CF5A64-2690-4BF1-83C9-66C9CF8C041D};Suomen ympäristökeskus/Vesikeskus;https://metadata.ymparisto.fi/dataset/{79CF5A64-2690-4BF1-83C9-66C9CF8C041D};; -Vesienhoitoalueiden seuranta - VHS Seuranta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F2FAD430-3632-4ACC-B2B1-70680795361E};Saatavuutta rajoitettu;{F2FAD430-3632-4ACC-B2B1-70680795361E};{F2FAD430-3632-4ACC-B2B1-70680795361E};Suomen ympäristökeskus / Vesikeskus / Sisävesiyksikkö;https://metadata.ymparisto.fi/dataset/{F2FAD430-3632-4ACC-B2B1-70680795361E};; -vesienhoitoalueiden seurantapaikat, Pohjavesi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3E07BDDC-AB68-4CDB-A647-BA6FCA784AB3};Saatavuutta rajoitettu;{3E07BDDC-AB68-4CDB-A647-BA6FCA784AB3};{5168AC0A-DD96-4231-9B4A-26DEAC055DC4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5168AC0A-DD96-4231-9B4A-26DEAC055DC4};; -Vesienhoitoalueiden seurantapaikat. Pintavesi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={22E6CAF3-20A9-44D1-B054-E019FEF882D4};Saatavuutta rajoitettu;{22E6CAF3-20A9-44D1-B054-E019FEF882D4};;;https://metadata.ymparisto.fi/dataset/;; -Vesihuollon tietojärjestelmä VEETI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F363ACA2-6CF5-46EF-907C-8F6CAF50C17B};Saatavuutta rajoitettu;{F363ACA2-6CF5-46EF-907C-8F6CAF50C17B};{373B7BEC-F23D-4AEF-9194-A646006F95AD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{373B7BEC-F23D-4AEF-9194-A646006F95AD};; -Vesihuoltolaitosten tilastointijärjestelmä VELVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DA001EC6-DB26-4339-AB97-DAB8FC2968B3};Saatavuutta rajoitettu;{DA001EC6-DB26-4339-AB97-DAB8FC2968B3};;;https://metadata.ymparisto.fi/dataset/;; -Vesihuoltoverkoston potentiaali (jätevesi, talousvesi ja jätevesi);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={479C025B-3ECC-4E97-8FF0-23E82B5A521B};Saatavuutta rajoitettu;{479C025B-3ECC-4E97-8FF0-23E82B5A521B};{363584D7-C854-4A20-BD7E-A1385B4E171D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{363584D7-C854-4A20-BD7E-A1385B4E171D};; -Vesiliikennerajoitukset-karttapalvelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2AD52986-F67E-4417-B733-BF57C94B4F02};Saatavuutta rajoitettu;{2AD52986-F67E-4417-B733-BF57C94B4F02};{5E05F2A7-17C5-44D3-99E2-68C263D3DD32};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5E05F2A7-17C5-44D3-99E2-68C263D3DD32};; -Vesimuodostumat-tietojärjestelmä VEMU;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6CD780AB-790D-4DA8-8962-9C8186B4127D};Saatavuutta rajoitettu;{6CD780AB-790D-4DA8-8962-9C8186B4127D};{6CD780AB-790D-4DA8-8962-9C8186B4127D};Suomen ympäristökeskus / Vesikeskus / Vesivarayksikkö;https://metadata.ymparisto.fi/dataset/{6CD780AB-790D-4DA8-8962-9C8186B4127D};; -Vesistömallijärjestelmä (WSFS-VEMALA);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C08C0416-6605-4BC5-8956-58AAF95F0030};Saatavuutta rajoitettu;{C08C0416-6605-4BC5-8956-58AAF95F0030};;;https://metadata.ymparisto.fi/dataset/;; -Vesistötyöt VESTY - Vesistöhankkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CA4578AC-3B2E-488F-8A0D-C91E470DAE49};Saatavuutta rajoitettu;{CA4578AC-3B2E-488F-8A0D-C91E470DAE49};{789B889E-6D94-413F-BD07-F811137637B7};Lasse Järvenpää;https://metadata.ymparisto.fi/dataset/{789B889E-6D94-413F-BD07-F811137637B7};; -Vesistötyöt VESTY - Rakenteet ja toimenpiteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={54CB6655-3038-4FAC-9822-5BADA622570F};Saatavuutta rajoitettu;{54CB6655-3038-4FAC-9822-5BADA622570F};{C13F44E5-E626-4AE5-BD23-EDCC1A21CDA9};Suomen ympäristökeskus / Vesikeskus / Vesivarayksikkö;https://metadata.ymparisto.fi/dataset/{C13F44E5-E626-4AE5-BD23-EDCC1A21CDA9};; -Vesivarat - käyttötoiminta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6F7ADB6E-193B-47A2-A4F6-2320D66D706A};Saatavuutta rajoitettu;{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};; -Vesiviljelyn sijainninohjaussuunnitelma;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9B1F82F-D696-49A1-9CC8-B69EEE5D61FF};Saatavuutta rajoitettu;{E9B1F82F-D696-49A1-9CC8-B69EEE5D61FF};;Maa- ja metsätalousministeriö, Ruokavirasto;https://metadata.ymparisto.fi/dataset/;; -Väestötietojärjestelmän rakennus- ja huoneistotiedot RHR;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={18C8309E-3963-4738-B6D2-96BA825344DC};Saatavuutta rajoitettu;{18C8309E-3963-4738-B6D2-96BA825344DC};;Digi- ja väestötietovirasto;https://metadata.ymparisto.fi/dataset/;; -Vähittäiskaupan toimipaikat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2A5BD858-ECAB-4E8B-B3B1-7FD0FE0DA798};Saatavuutta rajoitettu;{2A5BD858-ECAB-4E8B-B3B1-7FD0FE0DA798};;Tilastokeskus;https://metadata.ymparisto.fi/dataset/;; -Yleiskaavarekisteri - Yleiskaava MOPO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={34E2147E-D704-408D-A053-F91EEE20F94F};Saatavuutta rajoitettu;{34E2147E-D704-408D-A053-F91EEE20F94F};;;https://metadata.ymparisto.fi/dataset/;; -Ympäristöhallinnon kirjastojen kokoelma- ja artikkelitietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={51D1BC1D-93E5-4369-8019-630ACDEF53BA};Saatavuutta rajoitettu;{51D1BC1D-93E5-4369-8019-630ACDEF53BA};{51D1BC1D-93E5-4369-8019-630ACDEF53BA};Suomen ympäristökeskus / Tietokeskus / Tietopalvelu ja kirjasto;https://metadata.ymparisto.fi/dataset/{51D1BC1D-93E5-4369-8019-630ACDEF53BA};; -Ympäristöhallinnon kirjastojen lehtitietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B313B6FD-1CAB-41DD-9EF8-CF602F361773};Saatavuutta rajoitettu;{B313B6FD-1CAB-41DD-9EF8-CF602F361773};{B313B6FD-1CAB-41DD-9EF8-CF602F361773};Suomen ympäristökeskus / Tietokeskus / Tietopalvelu ja kirjasto;https://metadata.ymparisto.fi/dataset/{B313B6FD-1CAB-41DD-9EF8-CF602F361773};; -Ympäristökarttapalvelu karpalo;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7FB975C-846E-451A-9F5E-00EE3D2383AA};Saatavuutta rajoitettu;{B7FB975C-846E-451A-9F5E-00EE3D2383AA};{1334BB17-D27D-4876-9362-EF59256CC6BD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{1334BB17-D27D-4876-9362-EF59256CC6BD};; +title;vanha url;access;vanha guid;uusi guid;Omistaja;uusi url;; +Asuinalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E4DBC005-823B-469F-9AAB-51450438F41A};Avoin;{E4DBC005-823B-469F-9AAB-51450438F41A};{0B77FA8C-7C42-4783-AA5C-868152E32BE0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0B77FA8C-7C42-4783-AA5C-868152E32BE0};; +Chlorophyll-a daily product for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C6DCCB67-99B6-449A-A36F-26A847E6D856};Avoin;{C6DCCB67-99B6-449A-A36F-26A847E6D856};{10962669-987F-4FFA-9D73-A96E61AD6992};Finnish Environment Intitute (SYKE);https://metadata.ymparisto.fi/dataset/{10962669-987F-4FFA-9D73-A96E61AD6992};; +Chlorophyll-a daily product for the Baltic Sea 2012-2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={681B6DFF-2B4C-4881-AF69-37AD15A2B16A};Avoin;{681B6DFF-2B4C-4881-AF69-37AD15A2B16A};;;https://metadata.ymparisto.fi/dataset/;; +Chlorophyll-a weekly composite for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={422E4BF9-D15B-4779-8ABD-3610BD645C5C};Avoin;{422E4BF9-D15B-4779-8ABD-3610BD645C5C};;;https://metadata.ymparisto.fi/dataset/;; +Corine maanpeite 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A8936D40-2159-468D-84CD-0A978609B968};Avoin;{A8936D40-2159-468D-84CD-0A978609B968};{A70BC25F-43A2-4C33-A42D-9C22D9D4A3DB};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A70BC25F-43A2-4C33-A42D-9C22D9D4A3DB};; +Corine maanpeite 2006;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4438F7E1-2927-4854-B8F8-0EE8E8822C53};Avoin;{4438F7E1-2927-4854-B8F8-0EE8E8822C53};{703D5B2B-4CCC-49A7-B9E9-92B2E1290CD3};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{703D5B2B-4CCC-49A7-B9E9-92B2E1290CD3};; +Corine maanpeite 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D54C552F-F7F7-489B-8B1E-E093D93C7386};Avoin;{D54C552F-F7F7-489B-8B1E-E093D93C7386};{66D9A881-EE3C-42AD-9416-014EA6B84D23};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{66D9A881-EE3C-42AD-9416-014EA6B84D23};; +Harva ja tiheä taajama-alue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={674D6DB4-5FD9-434A-855F-F51316704630};Avoin;{674D6DB4-5FD9-434A-855F-F51316704630};{394B169F-2AE6-4966-8055-C593488F8898};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{394B169F-2AE6-4966-8055-C593488F8898};; +Havaitut tulva-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={62C30F1D-570D-4C75-80AE-6FE5402ECBBE};Avoin;{62C30F1D-570D-4C75-80AE-6FE5402ECBBE};{D8DE03E4-3F49-4BC1-9C58-3FADFB0F369A};;https://metadata.ymparisto.fi/dataset/{D8DE03E4-3F49-4BC1-9C58-3FADFB0F369A};; +Hydrologiset havaintopaikat, haihdunta-asemat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={72C2B39E-E581-4B5B-8C28-E5E0190DD97F};Avoin;{72C2B39E-E581-4B5B-8C28-E5E0190DD97F};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, jäänpaksuus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AB16A1FF-65E1-4A6E-A73D-653DDAAA7495};Avoin;{AB16A1FF-65E1-4A6E-A73D-653DDAAA7495};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, lumilinjamittaus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3D7A5268-E65C-4AB1-9A5E-99C8909AD764};Avoin;{3D7A5268-E65C-4AB1-9A5E-99C8909AD764};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, lämpötilaluotaus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={854C80BE-3383-4E67-8C21-0E4D038B7BA8};Avoin;{854C80BE-3383-4E67-8C21-0E4D038B7BA8};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, pintaveden lämpötila;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8462E50B-16BD-4C7F-9A79-F3DC6BFA1080};Avoin;{8462E50B-16BD-4C7F-9A79-F3DC6BFA1080};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, valunta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E11A44F-4987-4FF2-B55B-A3F94739FEE5};Avoin;{1E11A44F-4987-4FF2-B55B-A3F94739FEE5};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, vedenkorkeus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FC8A217-6A9D-4FE2-8923-843DA4A7DBE5};Avoin;{6FC8A217-6A9D-4FE2-8923-843DA4A7DBE5};;;https://metadata.ymparisto.fi/dataset/;; +Hydrologiset havaintopaikat, virtaama;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B6AFC4CE-7827-46BE-9372-141CE59394AA};Avoin;{B6AFC4CE-7827-46BE-9372-141CE59394AA};;;https://metadata.ymparisto.fi/dataset/;; +INSPIRE-tietotuote: Luonnonsuojelu- ja erämaa-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DFC5480E-CFC4-4FF3-B42F-9685481309BC};Avoin;{DFC5480E-CFC4-4FF3-B42F-9685481309BC};;;https://metadata.ymparisto.fi/dataset/;; +INSPIRE-tietotuote: Natura 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EB7FEE73-535C-49D8-AD0B-EAF13E52E665};Avoin;{EB7FEE73-535C-49D8-AD0B-EAF13E52E665};;;https://metadata.ymparisto.fi/dataset/;; +INSPIRE-tietotuote: Uomaverkosto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6AF05F3B-0D56-49D5-BFEA-F1652620D879};Avoin;{6AF05F3B-0D56-49D5-BFEA-F1652620D879};;;https://metadata.ymparisto.fi/dataset/;; +Itämeren päivittäiset pintalämpötilat / Sea surface temperature for the Baltic Sea;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4568E834-419F-4295-8CB1-2BE76C1F3318};Avoin;{4568E834-419F-4295-8CB1-2BE76C1F3318};;;https://metadata.ymparisto.fi/dataset/;; +Itämeren viikottaiset pintalämpötilat / Sea surface temperature weekly composites for the Baltic Sea;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1B235B6E-170C-4EF4-BE20-77C606F1A165};Avoin;{1B235B6E-170C-4EF4-BE20-77C606F1A165};;;https://metadata.ymparisto.fi/dataset/;; +Järvien ja jokien luotauspisteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={28041FC2-F80F-43FF-BEAA-CE66D449D04F};Avoin;{28041FC2-F80F-43FF-BEAA-CE66D449D04F};{80B4FB0F-C60E-4290-8183-1309E8AD8B76};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{80B4FB0F-C60E-4290-8183-1309E8AD8B76};; +Järvien ja jokien syvyysaineisto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E3647EED-2697-4EDE-92C9-900DD458B87F};Avoin;{E3647EED-2697-4EDE-92C9-900DD458B87F};{DC4AD1E9-F4E5-4998-825D-D2CA4836A2F5};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{DC4AD1E9-F4E5-4998-825D-D2CA4836A2F5};; +Kaupunki-maaseutu-luokitus (YKR);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BDD349C3-3656-4B3C-A590-77901C058DA2};Avoin;{BDD349C3-3656-4B3C-A590-77901C058DA2};{A2556B8B-0E17-4E70-AF87-0FCEBDADBE89};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A2556B8B-0E17-4E70-AF87-0FCEBDADBE89};; +Keskustat ja kaupan alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7D2F780B-39D7-4C83-9A2A-C3E3D6534B61};Avoin;{7D2F780B-39D7-4C83-9A2A-C3E3D6534B61};{79B6DD92-3448-402D-95A9-D56B484CD6A1};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{79B6DD92-3448-402D-95A9-D56B484CD6A1};; +Koskiensuojelulailla suojellut vesistöt;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={48CD102E-D5CE-445A-B9DC-74F8E9345D94};Avoin;{48CD102E-D5CE-445A-B9DC-74F8E9345D94};{CF5DEA48-E8E4-49AA-8EDB-DBE862B92F07};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CF5DEA48-E8E4-49AA-8EDB-DBE862B92F07};; +Lajit, luontodirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4E1FF620-E911-4678-83A4-F4A13944BD1A};Avoin;{4E1FF620-E911-4678-83A4-F4A13944BD1A};{4F51E747-E3DC-4223-BD0D-50108396F8A1};;https://metadata.ymparisto.fi/dataset/{4F51E747-E3DC-4223-BD0D-50108396F8A1};; +Lake Chl-a products from Finland (MERIS, FRESHMON);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=CHL_Finland_SYKE_2006-2011_MERIS_FUB_RECT;Avoin;CHL_Finland_SYKE_2006-2011_MERIS_FUB_RECT;;;https://metadata.ymparisto.fi/dataset/;; +Liikennealueiden ympäristöriskiluokitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F41D131D-B103-4726-B291-E43B49D136A2};Avoin;{F41D131D-B103-4726-B291-E43B49D136A2};{0143CB1F-4203-4252-B042-E32CB570A930};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0143CB1F-4203-4252-B042-E32CB570A930};; +Linnut, lintudirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={084531FC-EE76-418A-9832-5F8B418BA60E};Avoin;{084531FC-EE76-418A-9832-5F8B418BA60E};{EEE47DAE-72EF-403A-AED4-7F5E78E53739};;https://metadata.ymparisto.fi/dataset/{EEE47DAE-72EF-403A-AED4-7F5E78E53739};; +Luonnonsuojelu- ja erämaa-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2627E9FE-B657-48E1-A98D-000D4CD5CA38};Avoin;{2627E9FE-B657-48E1-A98D-000D4CD5CA38};{C8FC4A42-A2C3-40C4-92CD-2299C688514E};Metsähallitus;https://metadata.ymparisto.fi/dataset/{C8FC4A42-A2C3-40C4-92CD-2299C688514E};; +Luonnonsuojeluohjelma-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C305FA65-F319-4FA0-AAB8-F92AE32B6EE2};Avoin;{C305FA65-F319-4FA0-AAB8-F92AE32B6EE2};{5A93FF5B-FB88-40C7-84E0-91FCC492A621};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5A93FF5B-FB88-40C7-84E0-91FCC492A621};; +Luontotyypit, luontodirektiivin raportointi 2013;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B67BC862-E17C-42A2-B7AE-70B8A171170F};Avoin;{B67BC862-E17C-42A2-B7AE-70B8A171170F};{A0DF9C40-CC7B-4359-8595-5C76B760257C};;https://metadata.ymparisto.fi/dataset/{A0DF9C40-CC7B-4359-8595-5C76B760257C};; +Maasto- ja vesiliikenteen rajoitusalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={01A88DB7-7672-4671-95C4-3CD2AEAC5A99};Avoin;{01A88DB7-7672-4671-95C4-3CD2AEAC5A99};{AFBC7507-2167-4224-8CBA-37E1B6C975A3};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AFBC7507-2167-4224-8CBA-37E1B6C975A3};; +Maisemamaakuntajako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8D4119C7-23C6-419C-B874-2FB4C8DAEC44};Avoin;{8D4119C7-23C6-419C-B874-2FB4C8DAEC44};{27ACED25-8784-484D-962B-3207459D8BF0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{27ACED25-8784-484D-962B-3207459D8BF0};; +Metsäkasvillisuusvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={56486ECE-92E9-4FA5-96F3-0FC1D1B8C428};Avoin;{56486ECE-92E9-4FA5-96F3-0FC1D1B8C428};{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};; +Metsäkasvillisuusvyöhykkeet uhanalaisarviointia varten;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FBF9037A-3381-47C4-AD09-BBEAB007FEC0};Avoin;{FBF9037A-3381-47C4-AD09-BBEAB007FEC0};{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};;https://metadata.ymparisto.fi/dataset/{664BE696-C6A5-4FC4-8D6A-7D2E63D0E9C6};; +Natura2000 alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={36FA93DC-F156-427E-973E-E83F7AFC6306};Avoin;{36FA93DC-F156-427E-973E-E83F7AFC6306};{ED80465E-135B-4391-AA8A-FE2038FB224D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{ED80465E-135B-4391-AA8A-FE2038FB224D};; +Pohjavesialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F8321822-0A0D-4AC4-81E3-B9E3BE6386A2};Avoin;{F8321822-0A0D-4AC4-81E3-B9E3BE6386A2};{8F45F7BF-669F-4434-A8DB-8E686933F6FF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8F45F7BF-669F-4434-A8DB-8E686933F6FF};; +Rakennussuojelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1500436E-FD0E-4560-A933-847F16EFCC94};Avoin;{1500436E-FD0E-4560-A933-847F16EFCC94};{46AC1915-EC9D-4A02-92C0-58DAD8301E4A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{46AC1915-EC9D-4A02-92C0-58DAD8301E4A};; +Ranta10 - rantaviiva 1:10 000 ja uomaverkosto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A40A94CB-4905-4489-9C03-52B6CE9F66CD};Avoin;{A40A94CB-4905-4489-9C03-52B6CE9F66CD};{E15CD0B3-3934-4ABC-BF23-A8C622FA6A57};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E15CD0B3-3934-4ABC-BF23-A8C622FA6A57};; +Residential Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={593D9E64-4289-4B7B-A946-7A59C763C904};Avoin;{593D9E64-4289-4B7B-A946-7A59C763C904};;;https://metadata.ymparisto.fi/dataset/;; +Satelliittikuvamosaiikki (Image2000);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AF0A925D-9410-4431-8DB8-0455C73D193E};Avoin;{AF0A925D-9410-4431-8DB8-0455C73D193E};{0FD2F424-2C22-4384-A0C6-65AF19DED2A9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0FD2F424-2C22-4384-A0C6-65AF19DED2A9};; +Satelliittikuvamosaiikki (Image2006);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F1B27979-9B92-4607-B97F-28A5808AD5CC};Avoin;{F1B27979-9B92-4607-B97F-28A5808AD5CC};{5F95B099-0EAC-44DC-A1D5-B5B4A388A862};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5F95B099-0EAC-44DC-A1D5-B5B4A388A862};; +Satelliittikuvamosaiikki (Image2012);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={85FE6E30-90D2-4DB9-901B-7D86A74599DD};Avoin;{85FE6E30-90D2-4DB9-901B-7D86A74599DD};{94D48E72-5150-4D21-86E6-361506DAD28A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{94D48E72-5150-4D21-86E6-361506DAD28A};; +Siltojen korkeudet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7E8EA280-EAD4-4AFF-BA43-73DCA7805836};Avoin;{7E8EA280-EAD4-4AFF-BA43-73DCA7805836};{07943EAC-6AF9-4EB1-9B31-B47199446476};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{07943EAC-6AF9-4EB1-9B31-B47199446476};; +Soidensuojelun täydennysehdotus, Etelä-Suomi ja valtionmaan toteutuneet kohteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={5F14DCDF-6A2C-4A67-9035-BC73D0ED60B0};Avoin;{5F14DCDF-6A2C-4A67-9035-BC73D0ED60B0};{C0D331FF-68D8-4BD4-AEB3-B631AF824745};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C0D331FF-68D8-4BD4-AEB3-B631AF824745};; +Suokasvillisuusvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B6BD1CAD-BAB5-4473-AFD0-79DB30A88420};Avoin;{B6BD1CAD-BAB5-4473-AFD0-79DB30A88420};{193219CE-197F-4717-8D40-E7EDF6884A1E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{193219CE-197F-4717-8D40-E7EDF6884A1E};; +Tulvariskialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={83B57B72-1D1D-4DE4-BC61-180F3EE028AF};Avoin;{83B57B72-1D1D-4DE4-BC61-180F3EE028AF};{AF913A8D-484D-4313-ABD4-AE65A46A391E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AF913A8D-484D-4313-ABD4-AE65A46A391E};; +Tulvavaaravyöhykkeet, meritulva;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9EC02206-E14C-4186-AF09-829FFFB43FD9};Avoin;{9EC02206-E14C-4186-AF09-829FFFB43FD9};;;https://metadata.ymparisto.fi/dataset/;; +Tulvavaaravyöhykkeet, vesistötulva (avovesi);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C2B3FC73-060B-4053-BA04-54D86F4DE7E5};Avoin;{C2B3FC73-060B-4053-BA04-54D86F4DE7E5};;;https://metadata.ymparisto.fi/dataset/;; +Tunturialueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A9B5E4AF-21B3-4FA5-AF45-2CD819CE742E};Avoin;{A9B5E4AF-21B3-4FA5-AF45-2CD819CE742E};{ADEAFCBE-9E2D-403A-8E78-13FE913D5E42};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{ADEAFCBE-9E2D-403A-8E78-13FE913D5E42};; +Uimavesidirektiivin mukaiset uimavedet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={918099FC-918C-4FA3-9444-5A3D5E950877};Avoin;{918099FC-918C-4FA3-9444-5A3D5E950877};{11F3E166-1B35-4628-BE18-45D1A09A097F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{11F3E166-1B35-4628-BE18-45D1A09A097F};; +Valtakunnallisesti arvokkaat kallioalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4CFB1D5B-F47E-4E92-918A-63EC5BB9C217};Avoin;{4CFB1D5B-F47E-4E92-918A-63EC5BB9C217};{626BAE8B-6632-43FD-8B3A-CF1E01662331};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{626BAE8B-6632-43FD-8B3A-CF1E01662331};; +Valtakunnallisesti arvokkaat moreenimuodostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A21026BF-204C-42FC-9FCB-450BD5A94C81};Avoin;{A21026BF-204C-42FC-9FCB-450BD5A94C81};{8253487E-FA94-4E83-9A85-71F85D4DA0B0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8253487E-FA94-4E83-9A85-71F85D4DA0B0};; +Valtakunnallisesti arvokkaat tuuli- ja rantakerrostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={66A5653E-A8E7-4BA3-9703-A0AF001AE089};Avoin;{66A5653E-A8E7-4BA3-9703-A0AF001AE089};{3298587D-C391-47C6-A4E0-D599D0206B73};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3298587D-C391-47C6-A4E0-D599D0206B73};; +Valuma-aluejako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={343C40A6-D0ED-4B6A-BA77-2724A48A3BD3};Avoin;{343C40A6-D0ED-4B6A-BA77-2724A48A3BD3};{44394B13-85D7-4998-BD06-8ADC77C7455C};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{44394B13-85D7-4998-BD06-8ADC77C7455C};; +Water turbidity daily product for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={89F4653E-9930-4540-BFBA-C78444E1FCD5};Avoin;{89F4653E-9930-4540-BFBA-C78444E1FCD5};{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};Finnish Environment Intitute (SYKE);https://metadata.ymparisto.fi/dataset/{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};; +Water turbidity daily product for the Baltic Sea 2012-2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B4783610-3298-4550-9793-9DBDCB0080B1};Avoin;{B4783610-3298-4550-9793-9DBDCB0080B1};;;https://metadata.ymparisto.fi/dataset/;; +Water turbidity weekly composite for the Baltic Sea 2003-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CEF2EA78-0400-46F1-8173-CD1D1EE547D1};Avoin;{CEF2EA78-0400-46F1-8173-CD1D1EE547D1};{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};;https://metadata.ymparisto.fi/dataset/{5241F4C8-1F05-4AF0-9A0C-677EBA6BD4A6};; +Vesienhoitoalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DFA8CF50-721B-4C71-93DD-A8264C5C2A9C};Avoin;{DFA8CF50-721B-4C71-93DD-A8264C5C2A9C};{6AF42DE4-EB9A-4568-9E6C-90BEF19673D0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6AF42DE4-EB9A-4568-9E6C-90BEF19673D0};; +Vesipuitedirektiivin mukaiset vesimuodostumat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CB8FEF32-5FCE-4E33-A8B8-1FBB02F9C2C1};Avoin;{CB8FEF32-5FCE-4E33-A8B8-1FBB02F9C2C1};{A3748B80-6E63-4460-A8B7-72CB9884F69B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A3748B80-6E63-4460-A8B7-72CB9884F69B};; +Yhdyskuntarakenteen vyöhykkeet 2015;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={96F338EA-75AF-432C-A780-31A3CDECBDF2};Avoin;{96F338EA-75AF-432C-A780-31A3CDECBDF2};{3B8BD61A-C137-46ED-B16A-B8BA27D1561B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3B8BD61A-C137-46ED-B16A-B8BA27D1561B};; +YKR Kaupunkiseudut;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={29205135-177D-4244-ABF3-BEE74782E567};Avoin;{29205135-177D-4244-ABF3-BEE74782E567};{E3E29357-4333-4C11-8E0E-FCD7307F91EE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E3E29357-4333-4C11-8E0E-FCD7307F91EE};; +YKR-aluejaot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={802F4259-8AB3-473A-810C-FA3BE4FE5C92};Avoin;{802F4259-8AB3-473A-810C-FA3BE4FE5C92};{A2CEE8A2-F188-4787-88FC-A393032CA60A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A2CEE8A2-F188-4787-88FC-A393032CA60A};; +YKR-taajama;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CB02E608-6E6B-417E-87DA-BD7C8CB53712};Avoin;{CB02E608-6E6B-417E-87DA-BD7C8CB53712};{28F79998-B5E2-4085-AF80-FB3FA5E9F9BA};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{28F79998-B5E2-4085-AF80-FB3FA5E9F9BA};; +Ympäristömeludirektiivin mukaiset melualueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9D872FA-3A6D-45C6-AD52-78E0724307A7};Avoin;{E9D872FA-3A6D-45C6-AD52-78E0724307A7};;;https://metadata.ymparisto.fi/dataset/;; +Ympäristömeludirektiivin mukaiset melualueet 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8B962E44-AB11-4D38-B178-76AB85B114BB};Avoin;{8B962E44-AB11-4D38-B178-76AB85B114BB};{FCF0230E-07C1-48FF-A762-B51F1332F1DC};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{FCF0230E-07C1-48FF-A762-B51F1332F1DC};; +Aluesadannan ja lumen aluevesiarvon laskenta-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9CC3C6F8-D834-4663-8424-D846CEE43F3D};Saatavuutta rajoitettu;{9CC3C6F8-D834-4663-8424-D846CEE43F3D};{248E7E3F-02A6-41AF-A977-5A4949966001};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{248E7E3F-02A6-41AF-A977-5A4949966001};; +ARA-rakennukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E97AFFB-BFDB-4077-BEDD-BE5A7D971C8E};Saatavuutta rajoitettu;{1E97AFFB-BFDB-4077-BEDD-BE5A7D971C8E};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Areas Included in National Conservation Programmes;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A0C487D4-968B-4553-9EFB-870D6D2A728C};Saatavuutta rajoitettu;{A0C487D4-968B-4553-9EFB-870D6D2A728C};;;https://metadata.ymparisto.fi/dataset/;; +Areas with Local Detailed Plans;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E60A3152-F0AA-4E61-BA1C-E28F0DBE8FF6};Saatavuutta rajoitettu;{E60A3152-F0AA-4E61-BA1C-E28F0DBE8FF6};;;https://metadata.ymparisto.fi/dataset/;; +Asemakaavoitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AC7E17C3-0833-40B1-B37A-187320A03826};Saatavuutta rajoitettu;{AC7E17C3-0833-40B1-B37A-187320A03826};{31968F40-0083-413A-B474-A6D2DB081EDF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{31968F40-0083-413A-B474-A6D2DB081EDF};; +Asemakaavojen seurantalomakkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9BCE41E0-54BC-43A4-9543-D3948FD151EF};Saatavuutta rajoitettu;{9BCE41E0-54BC-43A4-9543-D3948FD151EF};{9BCE41E0-54BC-43A4-9543-D3948FD151EF};Suomen ympäristökeskus / Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{9BCE41E0-54BC-43A4-9543-D3948FD151EF};; +Asianhallintajärjestelmä AHJO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DBBDEECB-AB2A-4B01-B45B-DE809992F79F};Saatavuutta rajoitettu;{DBBDEECB-AB2A-4B01-B45B-DE809992F79F};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Asuinrakennusten tiheys (rakennusta/km2);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={44F16A49-3DC8-4BB5-9420-6D015FB64EF1};Saatavuutta rajoitettu;{44F16A49-3DC8-4BB5-9420-6D015FB64EF1};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +AVOIMUUSINDEKSI MERENPOHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0189B2FD-4411-4CB0-90E5-DF4E1293C6F3};Saatavuutta rajoitettu;{0189B2FD-4411-4CB0-90E5-DF4E1293C6F3};{EB99F934-DCF8-4383-BD38-73B2DF3F0C72};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{EB99F934-DCF8-4383-BD38-73B2DF3F0C72};; +Bathing Waters;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1D154349-AFCA-4FBB-AD07-50EB6F36339D};Saatavuutta rajoitettu;{1D154349-AFCA-4FBB-AD07-50EB6F36339D};;;https://metadata.ymparisto.fi/dataset/;; +BCU-konttipaikat (öljyyntyneiden lintujen hoitoon);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EFC9B874-90BF-4701-BABF-29F6F7D58BE0};Saatavuutta rajoitettu;{EFC9B874-90BF-4701-BABF-29F6F7D58BE0};{DF0A5826-F1DF-42EB-A7A8-B8510A46B675};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{DF0A5826-F1DF-42EB-A7A8-B8510A46B675};; +BORIS 2 (Baltic Oil Response Information System);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={180ACCFE-FE84-4CD3-AF8A-6FF75C473059};Saatavuutta rajoitettu;{180ACCFE-FE84-4CD3-AF8A-6FF75C473059};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Catchment areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D6C6858A-562D-4965-AD77-2B1E97EFFA6B};Saatavuutta rajoitettu;{D6C6858A-562D-4965-AD77-2B1E97EFFA6B};;;https://metadata.ymparisto.fi/dataset/;; +CERAMIUM TENUICORNE LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D30F72F9-01F0-448A-9F18-213954BF87C7};Saatavuutta rajoitettu;{D30F72F9-01F0-448A-9F18-213954BF87C7};{F274E613-65AB-4867-B2C6-925596B7A8DE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F274E613-65AB-4867-B2C6-925596B7A8DE};; +CHARA ASPERA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F271AB26-3268-461F-A380-F3A1CC6AB62E};Saatavuutta rajoitettu;{F271AB26-3268-461F-A380-F3A1CC6AB62E};{BF849316-273C-4347-B151-D4F06460C314};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF849316-273C-4347-B151-D4F06460C314};; +CHARA SPP. & NITELLA SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={31CF9F00-FAA6-4158-B2E9-9D443776C4B1};Saatavuutta rajoitettu;{31CF9F00-FAA6-4158-B2E9-9D443776C4B1};;;https://metadata.ymparisto.fi/dataset/;; +CHARA TOMENTOSA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3895111B-8E40-4EC1-9256-B4C59B460832};Saatavuutta rajoitettu;{3895111B-8E40-4EC1-9256-B4C59B460832};{0408513A-8CCF-422F-A47B-F12F0946D6EA};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0408513A-8CCF-422F-A47B-F12F0946D6EA};; +CHORDA FILUM LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7D430B8-AEA0-4701-BD8F-533892EA0AD4};Saatavuutta rajoitettu;{B7D430B8-AEA0-4701-BD8F-533892EA0AD4};{CE550B14-5568-4C84-A67C-1473FC33E71F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CE550B14-5568-4C84-A67C-1473FC33E71F};; +CLADOPHORA GLOMERATA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={609B6F9F-954E-4F98-BCFC-172689200290};Saatavuutta rajoitettu;{609B6F9F-954E-4F98-BCFC-172689200290};{411FD599-AAC9-491D-95CA-BC942AE2FF72};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{411FD599-AAC9-491D-95CA-BC942AE2FF72};; +CLADOPHORA RUPESTRIS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0B5AAC11-E3E2-40D3-800F-8F5801F503AB};Saatavuutta rajoitettu;{0B5AAC11-E3E2-40D3-800F-8F5801F503AB};{37D789D3-A7E2-44FA-B227-58B59F962244};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{37D789D3-A7E2-44FA-B227-58B59F962244};; +Corine Land Cover 2000;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EEC35D81-765A-4A7B-BC9B-F89A476807EC};Saatavuutta rajoitettu;{EEC35D81-765A-4A7B-BC9B-F89A476807EC};;;https://metadata.ymparisto.fi/dataset/;; +Corine Land Cover 2006;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DD648E4F-0478-4D32-8286-C2437EC87D33};Saatavuutta rajoitettu;{DD648E4F-0478-4D32-8286-C2437EC87D33};;;https://metadata.ymparisto.fi/dataset/;; +Corine Land Cover 2012;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A58329D8-3ED0-47B8-B789-3095EE55C255};Saatavuutta rajoitettu;{A58329D8-3ED0-47B8-B789-3095EE55C255};;;https://metadata.ymparisto.fi/dataset/;; +Elinympäristön tietopalvelu Liiteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={629AC114-8BBC-48B5-9D8F-303A53CF3E9F};Saatavuutta rajoitettu;{629AC114-8BBC-48B5-9D8F-303A53CF3E9F};{1893C260-D934-48F8-A0A0-3606FC392438};Suomen ympäristökeskus / Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{1893C260-D934-48F8-A0A0-3606FC392438};; +Eliölajit-tietojärjestelmä TAXON;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C420898F-600A-41CB-8843-F06A90301427};Saatavuutta rajoitettu;{C420898F-600A-41CB-8843-F06A90301427};{C420898F-600A-41CB-8843-F06A90301427};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C420898F-600A-41CB-8843-F06A90301427};; +Energiantuotanto rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9707160B-8B54-48DB-9151-4A41FD46D159};Saatavuutta rajoitettu;{9707160B-8B54-48DB-9151-4A41FD46D159};{DBEAADBD-3EE0-498D-8EC2-C66191DA559C};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{DBEAADBD-3EE0-498D-8EC2-C66191DA559C};; +Environmental Noise Maps;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0CF94C00-D59A-45A0-A2EA-4ACF9014A2BA};Saatavuutta rajoitettu;{0CF94C00-D59A-45A0-A2EA-4ACF9014A2BA};;;https://metadata.ymparisto.fi/dataset/;; +EPHYDATIA FLUVIATILIS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB5AE7E2-8C84-4FCC-83DD-1150F60EEE27};Saatavuutta rajoitettu;{BB5AE7E2-8C84-4FCC-83DD-1150F60EEE27};{20637AA7-97D7-43AB-AF34-636043F1C1B9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{20637AA7-97D7-43AB-AF34-636043F1C1B9};; +Etäisyysvyöhykkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={64764F78-0F35-438B-9EA4-FDAC3384840D};Saatavuutta rajoitettu;{64764F78-0F35-438B-9EA4-FDAC3384840D};{BF51D501-58F4-4932-A951-5B5A88F4D3C6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF51D501-58F4-4932-A951-5B5A88F4D3C6};; +Flood hazard areas, lake and river floods;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={302A9608-99B9-4D10-A469-36949D12436F};Saatavuutta rajoitettu;{302A9608-99B9-4D10-A469-36949D12436F};;;https://metadata.ymparisto.fi/dataset/;; +Flood hazard areas, sea floods;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6C21A825-FE70-47CE-91CA-E14EBE01F562};Saatavuutta rajoitettu;{6C21A825-FE70-47CE-91CA-E14EBE01F562};;;https://metadata.ymparisto.fi/dataset/;; +Flood Risk Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={13972098-7333-4827-B5D0-CFEF3CE54295};Saatavuutta rajoitettu;{13972098-7333-4827-B5D0-CFEF3CE54295};;;https://metadata.ymparisto.fi/dataset/;; +FONTINALIS SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={83CE79F8-4DF5-4B8A-B431-77FAC6195603};Saatavuutta rajoitettu;{83CE79F8-4DF5-4B8A-B431-77FAC6195603};{6EF6BCA9-D30B-45C1-8111-641DC55FA095};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6EF6BCA9-D30B-45C1-8111-641DC55FA095};; +Forest Vegetation Zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D53E9674-A446-4F54-81C3-8C15310353CC};Saatavuutta rajoitettu;{D53E9674-A446-4F54-81C3-8C15310353CC};;;https://metadata.ymparisto.fi/dataset/;; +FUCUS SPP. LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FB02515B-D7A9-4F6D-BD15-2E2B818AD997};Saatavuutta rajoitettu;{FB02515B-D7A9-4F6D-BD15-2E2B818AD997};{B6B48CE3-B9FA-442A-9FF5-F8E0B396578D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{B6B48CE3-B9FA-442A-9FF5-F8E0B396578D};; +FUCUS-YHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={76879C3D-8293-4C12-B873-26F59BCF54B9};Saatavuutta rajoitettu;{76879C3D-8293-4C12-B873-26F59BCF54B9};;;https://metadata.ymparisto.fi/dataset/;; +GOFREP-alue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B9487289-0C02-48E4-A26F-DAACDA60C462};Saatavuutta rajoitettu;{B9487289-0C02-48E4-A26F-DAACDA60C462};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Groundwater areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FA1742D1-8509-437C-846A-6637B3FF7345};Saatavuutta rajoitettu;{FA1742D1-8509-437C-846A-6637B3FF7345};;;https://metadata.ymparisto.fi/dataset/;; +HAURA- JA HAPSIKKAPOHJAT LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FF12F76-3162-4DD9-8320-668EDB9CBD9E};Saatavuutta rajoitettu;{6FF12F76-3162-4DD9-8320-668EDB9CBD9E};{C422872A-BA11-406C-9263-44D3302A7FCD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C422872A-BA11-406C-9263-44D3302A7FCD};; +Helsingin ihmispaineindeksi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={43163BBB-0D01-4375-BE28-33AA1607B2C3};Saatavuutta rajoitettu;{43163BBB-0D01-4375-BE28-33AA1607B2C3};{2063FB74-4879-4213-B9B5-E33FC848315E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2063FB74-4879-4213-B9B5-E33FC848315E};; +Historic Flood Maps;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6FF5E8D4-5329-44D1-8741-D7F3223D0F30};Saatavuutta rajoitettu;{6FF5E8D4-5329-44D1-8741-D7F3223D0F30};;;https://metadata.ymparisto.fi/dataset/;; +HUB-biotooppiluokittelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7DB140D8-F133-42DB-B424-3309C2684A6D};Saatavuutta rajoitettu;{7DB140D8-F133-42DB-B424-3309C2684A6D};{E0001193-53A2-4CA3-B849-753621E51A69};Metsähallitus / Matti Sahla;https://metadata.ymparisto.fi/dataset/{E0001193-53A2-4CA3-B849-753621E51A69};; +Hydrologian ja vesien käytön tietojärjestelmä HYDRO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={86FC3188-6796-4C79-AC58-8DBC7B568827};Saatavuutta rajoitettu;{86FC3188-6796-4C79-AC58-8DBC7B568827};{86FC3188-6796-4C79-AC58-8DBC7B568827};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{86FC3188-6796-4C79-AC58-8DBC7B568827};; +Hätäkeskukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={51C9D60D-6D41-44BD-9136-C4933510DB2D};Saatavuutta rajoitettu;{51C9D60D-6D41-44BD-9136-C4933510DB2D};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Ilmapäästötietojärjestelmä IPTJ;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={033E574E-3B6D-4A40-A2BC-8C50C8FA86E0};Saatavuutta rajoitettu;{033E574E-3B6D-4A40-A2BC-8C50C8FA86E0};;;https://metadata.ymparisto.fi/dataset/;; +Jokisuistot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6DEF8642-7867-4047-AEB9-5134D24C4D64};Saatavuutta rajoitettu;{6DEF8642-7867-4047-AEB9-5134D24C4D64};{5B7F87B2-F082-4081-B653-3AEF12446C9E};Metsähallitus;https://metadata.ymparisto.fi/dataset/{5B7F87B2-F082-4081-B653-3AEF12446C9E};; +Järvirekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E4505311-A27A-4CD5-B207-89EFEEFE48CF};Saatavuutta rajoitettu;{E4505311-A27A-4CD5-B207-89EFEEFE48CF};{E4505311-A27A-4CD5-B207-89EFEEFE48CF};Suomen ympäristökeskus /Vesikeskus;https://metadata.ymparisto.fi/dataset/{E4505311-A27A-4CD5-B207-89EFEEFE48CF};; +Järvirekisteri / Säännöstelyhankkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};Saatavuutta rajoitettu;{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E6FB5DFE-CA39-4B50-AD36-ABF273B3C375};; +Jätevedenpuhdistamot rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CE26C069-1F54-4F78-8FE2-A1C025E8C9E8};Saatavuutta rajoitettu;{CE26C069-1F54-4F78-8FE2-A1C025E8C9E8};{E1663280-9639-4559-B0EA-18D1B4BD5625};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{E1663280-9639-4559-B0EA-18D1B4BD5625};; +Kaavoituksen seurannan tilasto;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4A29A502-D287-4956-AD84-8C467D1E4D40};Saatavuutta rajoitettu;{4A29A502-D287-4956-AD84-8C467D1E4D40};{4A29A502-D287-4956-AD84-8C467D1E4D40};Suomen ympäristökeskus/Ympäristöpolitiikkakeskus;https://metadata.ymparisto.fi/dataset/{4A29A502-D287-4956-AD84-8C467D1E4D40};; +Kalankasvatus ja verkkoaltaat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F4A6DADE-650B-4BA2-8E69-11ED37EC1335};Saatavuutta rajoitettu;{F4A6DADE-650B-4BA2-8E69-11ED37EC1335};{D2396FA4-DFF0-4D88-A647-70256C18941E};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{D2396FA4-DFF0-4D88-A647-70256C18941E};; +Kansainväliset jätesiirrot -rekisteri (Basel-rekisteri);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8D42F658-9DAD-4C65-9418-ABB00B92DF98};Saatavuutta rajoitettu;{8D42F658-9DAD-4C65-9418-ABB00B92DF98};{8D42F658-9DAD-4C65-9418-ABB00B92DF98};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8D42F658-9DAD-4C65-9418-ABB00B92DF98};; +Kansalliset kaupunkipuistot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={ECE5D7C1-FF3C-47C2-84F1-D878CD3DA6EE};Saatavuutta rajoitettu;{ECE5D7C1-FF3C-47C2-84F1-D878CD3DA6EE};{43E99E3C-7C91-42F0-A324-C9CB086E65DD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{43E99E3C-7C91-42F0-A324-C9CB086E65DD};; +Kasviplanktontietojärjestelmä KPLANK;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2F164F22-A4F9-468A-97E5-717B0B3D0E7B};Saatavuutta rajoitettu;{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};;https://metadata.ymparisto.fi/dataset/{2F164F22-A4F9-468A-97E5-717B0B3D0E7B};; +Kemikaalien ympäristötietorekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={04B79E94-8D3B-451E-963B-19DDAD6F6215};Saatavuutta rajoitettu;{04B79E94-8D3B-451E-963B-19DDAD6F6215};;;https://metadata.ymparisto.fi/dataset/;; +Kertymärekisteri KERTY;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FF5FE8B1-B2BC-44C7-8A47-744F0A522CD2};Saatavuutta rajoitettu;{FF5FE8B1-B2BC-44C7-8A47-744F0A522CD2};;;https://metadata.ymparisto.fi/dataset/;; +Koekalastusrekisteri (Nordic-verkot, Coastal-verkot, sähkökoekalastus);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={348DFB97-5F9F-4902-9152-23B0C9505673};Saatavuutta rajoitettu;{348DFB97-5F9F-4902-9152-23B0C9505673};;Luonnonvarakeskus;https://metadata.ymparisto.fi/dataset/;; +Kuntien poikkeamispäätökset ja suunnittelutarveratkaisut sekä ELY-keskusten tekemät poikkeamispäätökset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0A81369B-6693-4BEB-80EF-238035480C3C};Saatavuutta rajoitettu;{0A81369B-6693-4BEB-80EF-238035480C3C};;;https://metadata.ymparisto.fi/dataset/;; +Laboratorion ja näytteenoton tiedonhallintajärjestelmä LIMS;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={939AE4E5-332A-4B0C-B9BF-96E0F16620A0};Saatavuutta rajoitettu;{939AE4E5-332A-4B0C-B9BF-96E0F16620A0};;;https://metadata.ymparisto.fi/dataset/;; +Lake and River Depth Profiles;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={23FB0660-5B2B-4212-BE02-772EB6E27E16};Saatavuutta rajoitettu;{23FB0660-5B2B-4212-BE02-772EB6E27E16};;;https://metadata.ymparisto.fi/dataset/;; +Laskeumarekisteri;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={19002DDF-91D0-48A3-8E68-891C8E7374D5};Saatavuutta rajoitettu;{19002DDF-91D0-48A3-8E68-891C8E7374D5};;;https://metadata.ymparisto.fi/dataset/;; +Liikennealueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F47A597F-FC14-47C5-BD3A-AE0710BC8A08};Saatavuutta rajoitettu;{F47A597F-FC14-47C5-BD3A-AE0710BC8A08};{22C3E0C5-3221-408E-B346-60493CCE6361};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{22C3E0C5-3221-408E-B346-60493CCE6361};; +Lumen peittämä ala Itämeren alueella 2007-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=Metadata_SYKE_SCA_Baltic_0.05deg_pv;Saatavuutta rajoitettu;Metadata_SYKE_SCA_Baltic_0.05deg_pv;;;https://metadata.ymparisto.fi/dataset/;; +Lumen peittämä ala Suomessa 2006-2011;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=Metadata_SYKE_SCA_2006-2011_Finland;Saatavuutta rajoitettu;Metadata_SYKE_SCA_2006-2011_Finland;;;https://metadata.ymparisto.fi/dataset/;; +Maa-ainesluvat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={604DFF56-4157-46DE-84CB-71978DAC790A};Saatavuutta rajoitettu;{604DFF56-4157-46DE-84CB-71978DAC790A};{F163EC63-72A8-4FB5-A021-7A828BC1923F};KEHA-keskus;https://metadata.ymparisto.fi/dataset/{F163EC63-72A8-4FB5-A021-7A828BC1923F};; +Maaperän tilan tietojärjestelmä MATTI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};Saatavuutta rajoitettu;{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};Suomen ympäristökeskus ja ELY-keskukset;https://metadata.ymparisto.fi/dataset/{BB4FD42D-EDA1-4AC2-8D64-A4253EDCDB94};; +Maastoliikennerajoitukset-karttapalvelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E8222F77-00C4-4934-B2CB-0AB6A93D9E69};Saatavuutta rajoitettu;{E8222F77-00C4-4934-B2CB-0AB6A93D9E69};{8F7C8F06-B945-4D95-91BE-F0ECF1D76B7E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8F7C8F06-B945-4D95-91BE-F0ECF1D76B7E};; +Mahdolliset kapeat murtovesilahdet (1650);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8770CC62-F274-4700-9A81-592FBDE66635};Saatavuutta rajoitettu;{8770CC62-F274-4700-9A81-592FBDE66635};;;https://metadata.ymparisto.fi/dataset/;; +Mahdolliset laajat matalat lahdet (1160);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6655C0FA-9562-4015-B97A-0D07A19CF72C};Saatavuutta rajoitettu;{6655C0FA-9562-4015-B97A-0D07A19CF72C};;;https://metadata.ymparisto.fi/dataset/;; +Mahdolliset rannikon laguunit (1150);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={74345ACE-9D96-4A52-9E2F-7E8FE38C7809};Saatavuutta rajoitettu;{74345ACE-9D96-4A52-9E2F-7E8FE38C7809};;;https://metadata.ymparisto.fi/dataset/;; +MERENPOHJAN KIVIKKOISUUDEN TODENNÄKÖISYYS;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={41482CFB-D636-4396-AAB3-CBC82A28772A};Saatavuutta rajoitettu;{41482CFB-D636-4396-AAB3-CBC82A28772A};;;https://metadata.ymparisto.fi/dataset/;; +Merialuejako;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3F43EA83-48C3-4D04-9DDF-67822FB32EF2};Saatavuutta rajoitettu;{3F43EA83-48C3-4D04-9DDF-67822FB32EF2};{6030C360-861F-4C16-B4BB-6A0EB4B5CF96};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6030C360-861F-4C16-B4BB-6A0EB4B5CF96};; +Merihiekan- ja soranottoalueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={46D1A89C-7CE9-420E-B592-4018AC17EEE8};Saatavuutta rajoitettu;{46D1A89C-7CE9-420E-B592-4018AC17EEE8};{E35DE705-2986-43F2-8678-F219F1D097F2};Metsähallitus;https://metadata.ymparisto.fi/dataset/{E35DE705-2986-43F2-8678-F219F1D097F2};; +Meripelastusvastuualue;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A2C05340-3276-4FBB-8350-607560E3C31F};Saatavuutta rajoitettu;{A2C05340-3276-4FBB-8350-607560E3C31F};{22A9AD4A-E725-4426-BD1A-916A23CF73BE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{22A9AD4A-E725-4426-BD1A-916A23CF73BE};; +MERIVEDEN LÄMPÖTILA POHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={ED3D3E91-8674-4532-8E78-3E63950A9BE5};Saatavuutta rajoitettu;{ED3D3E91-8674-4532-8E78-3E63950A9BE5};{8E562E41-5465-40A1-9990-4EACE2537F01};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8E562E41-5465-40A1-9990-4EACE2537F01};; +Meriveden suolaisuus pinnalla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EA4227EC-6958-4EC1-AE3C-3051FB9678B8};Saatavuutta rajoitettu;{EA4227EC-6958-4EC1-AE3C-3051FB9678B8};{D9F7BA00-14B9-40A0-9FEB-A9549266D7FF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D9F7BA00-14B9-40A0-9FEB-A9549266D7FF};; +MERIVEDEN SUOLAISUUS POHJALLA (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7E5A9C8-08A2-4272-9AB2-ADA81A2E6C69};Saatavuutta rajoitettu;{B7E5A9C8-08A2-4272-9AB2-ADA81A2E6C69};;;https://metadata.ymparisto.fi/dataset/;; +Mire vegetation zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={79F1A09C-54C6-4EBE-96D8-1970459896EE};Saatavuutta rajoitettu;{79F1A09C-54C6-4EBE-96D8-1970459896EE};;;https://metadata.ymparisto.fi/dataset/;; +MRL:n mukaisten yleiskaavojen ulkorajaukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D8DD3305-0313-47F4-B241-71658B6C06AC};Saatavuutta rajoitettu;{D8DD3305-0313-47F4-B241-71658B6C06AC};;;https://metadata.ymparisto.fi/dataset/;; +MYTILUS TROSSULUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FECA3DEC-6C77-4424-BC42-0AE1452B2BA0};Saatavuutta rajoitettu;{FECA3DEC-6C77-4424-BC42-0AE1452B2BA0};{6AC94879-B22D-42DA-B3F5-3FC955AE03E4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6AC94879-B22D-42DA-B3F5-3FC955AE03E4};; +NAJAS MARINA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6906D7D5-BAEC-4006-8FDB-84732B61ADE7};Saatavuutta rajoitettu;{6906D7D5-BAEC-4006-8FDB-84732B61ADE7};{61E89CB2-03BA-4FD5-A231-BA2D909D30EF};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{61E89CB2-03BA-4FD5-A231-BA2D909D30EF};; +Nannut-luokittelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8A4E6487-B27A-4F3B-8EA4-4651EA39ACCE};Saatavuutta rajoitettu;{8A4E6487-B27A-4F3B-8EA4-4651EA39ACCE};{F60E6BF7-9BC6-476D-8102-2E310A204BE0};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F60E6BF7-9BC6-476D-8102-2E310A204BE0};; +National Database of Regional Land Use Plans;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2FAA5D6B-C053-465E-812C-119798581F5C};Saatavuutta rajoitettu;{2FAA5D6B-C053-465E-812C-119798581F5C};;;https://metadata.ymparisto.fi/dataset/;; +National Landscape Division;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={09F5613F-D9A2-4C68-818D-70B018437888};Saatavuutta rajoitettu;{09F5613F-D9A2-4C68-818D-70B018437888};;;https://metadata.ymparisto.fi/dataset/;; +Nationally Designated Nature Protected Areas and Wilderness Reserves;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9871C541-9E84-4241-A1E5-51C596A5A4E2};Saatavuutta rajoitettu;{9871C541-9E84-4241-A1E5-51C596A5A4E2};;;https://metadata.ymparisto.fi/dataset/;; +Nationally Valuable Aeolian and Littoral Deposits;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={04399CD4-86CD-453E-B683-5CFF53FA36D0};Saatavuutta rajoitettu;{04399CD4-86CD-453E-B683-5CFF53FA36D0};;;https://metadata.ymparisto.fi/dataset/;; +Nationally Valuable Moraine Formations;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0193799E-1F36-4FB9-A0B9-48264860C58C};Saatavuutta rajoitettu;{0193799E-1F36-4FB9-A0B9-48264860C58C};;;https://metadata.ymparisto.fi/dataset/;; +Nationally Valuable Rocky Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2753EE72-1F06-4291-987F-08FA693ED5CA};Saatavuutta rajoitettu;{2753EE72-1F06-4291-987F-08FA693ED5CA};;;https://metadata.ymparisto.fi/dataset/;; +Natura 2000 -tietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7AC30A3E-8E38-4717-8D2E-80585623212B};Saatavuutta rajoitettu;{7AC30A3E-8E38-4717-8D2E-80585623212B};;Suomen ympäristökeskus / Luontoympäristökeskus / Biodiversiteettiyksikkö;https://metadata.ymparisto.fi/dataset/;; +Nature Recreational Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9F0B1FD-C41E-46A1-BA30-E9A29DF12D70};Saatavuutta rajoitettu;{E9F0B1FD-C41E-46A1-BA30-E9A29DF12D70};;;https://metadata.ymparisto.fi/dataset/;; +NÄKÖSYVYYS (VELMU);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={55AB842F-9CED-4E80-A7E5-07A54F0AE4A4};Saatavuutta rajoitettu;{55AB842F-9CED-4E80-A7E5-07A54F0AE4A4};{811B434E-75A1-4F53-9445-A4B4BDADA499};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{811B434E-75A1-4F53-9445-A4B4BDADA499};; +Olemassa oleva ja suunniteltu tuulivoima;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9B856E2E-6F8F-4B9A-8618-9923F238852D};Saatavuutta rajoitettu;{9B856E2E-6F8F-4B9A-8618-9923F238852D};{F690781C-D187-4611-8D80-A703804F7B59};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F690781C-D187-4611-8D80-A703804F7B59};; +Oppilaitokset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7753FF3C-D1E8-453A-B3F1-8412A2AD1CD1};Saatavuutta rajoitettu;{7753FF3C-D1E8-453A-B3F1-8412A2AD1CD1};;Tilastokeskus;https://metadata.ymparisto.fi/dataset/;; +Outlines of Local Master Plans Ratified in Accordance to the old Building Act;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6771A0AD-3B9C-450E-9536-0B036B47A662};Saatavuutta rajoitettu;{6771A0AD-3B9C-450E-9536-0B036B47A662};;;https://metadata.ymparisto.fi/dataset/;; +Outlines of Local Master Plans Ratified in Accordance with the Landuse and Building Act;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2D70E79B-36F8-4E5E-BB6C-0DD7C6E5639D};Saatavuutta rajoitettu;{2D70E79B-36F8-4E5E-BB6C-0DD7C6E5639D};;;https://metadata.ymparisto.fi/dataset/;; +Paikkatietoaineistojen latauspalvelu LAPIO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DDFEA0F7-7A42-4E8E-9782-57DD74697BD1};Saatavuutta rajoitettu;{DDFEA0F7-7A42-4E8E-9782-57DD74697BD1};{3EEF491C-53D4-4614-B64C-1D98A699198C};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{3EEF491C-53D4-4614-B64C-1D98A699198C};; +Paikkatietoanalyysien tuloksia;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BBDCFF54-8109-4795-8BB5-15FA9480BB9C};Saatavuutta rajoitettu;{BBDCFF54-8109-4795-8BB5-15FA9480BB9C};{F039C4B7-7D3C-4EFA-99B0-C52091BC97B4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{F039C4B7-7D3C-4EFA-99B0-C52091BC97B4};; +Patoturvallisuuden tietojärjestelmä PATOTURVA;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={895902E7-031C-48AD-A8AC-DCE7D325B9EE};Saatavuutta rajoitettu;{895902E7-031C-48AD-A8AC-DCE7D325B9EE};;;https://metadata.ymparisto.fi/dataset/;; +Pelastustoimen alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={75182F9D-D7D5-4A3A-8556-1C96577F0A52};Saatavuutta rajoitettu;{75182F9D-D7D5-4A3A-8556-1C96577F0A52};{34A6DAEE-DE66-45A2-B3A1-CB23CF6FF45E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{34A6DAEE-DE66-45A2-B3A1-CB23CF6FF45E};; +PINNAN AALTOEKSPOSITIO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BB655D7F-1E25-4E19-AE7F-5DEBF1AEFC6C};Saatavuutta rajoitettu;{BB655D7F-1E25-4E19-AE7F-5DEBF1AEFC6C};{216DD75B-59A8-408A-80CD-F0785E548769};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{216DD75B-59A8-408A-80CD-F0785E548769};; +Pintavesien tilan tietojärjestelmä, vedenlaatu PIVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B1444E19-0F36-49F5-A849-01A3D2083A11};Saatavuutta rajoitettu;{B1444E19-0F36-49F5-A849-01A3D2083A11};;;https://metadata.ymparisto.fi/dataset/;; +Pohjaeläinhavaintoja POHJE-tietokannasta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2FA13650-7CB3-4299-8663-7542769D102E};Saatavuutta rajoitettu;{2FA13650-7CB3-4299-8663-7542769D102E};{0A22C866-3D0C-476A-9957-9275EB18B8AD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{0A22C866-3D0C-476A-9957-9275EB18B8AD};; +Pohjaeläintietojärjestelmä - POHJE;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};Saatavuutta rajoitettu;{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};;https://metadata.ymparisto.fi/dataset/{EA35908B-1AD2-4C93-B437-A3BB9ADA80F9};; +Pohjavesitietojärjestelmä POVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C594F22E-B642-434D-B6F2-6ECB03F760C7};Saatavuutta rajoitettu;{C594F22E-B642-434D-B6F2-6ECB03F760C7};{C594F22E-B642-434D-B6F2-6ECB03F760C7};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C594F22E-B642-434D-B6F2-6ECB03F760C7};; +Poronhoidon paikkatiedot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={12E7100A-C010-40A3-A441-4EED54C9FC64};Saatavuutta rajoitettu;{12E7100A-C010-40A3-A441-4EED54C9FC64};{A6E6BD51-E463-4E0A-88EC-71DBFDF2223E};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{A6E6BD51-E463-4E0A-88EC-71DBFDF2223E};; +POTAMOGETON PERFOLIATUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D7F3685D-0432-4FC3-B0D4-13E798050A67};Saatavuutta rajoitettu;{D7F3685D-0432-4FC3-B0D4-13E798050A67};{8FD8F68C-373B-4A47-BB76-2E9040F3C488};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{8FD8F68C-373B-4A47-BB76-2E9040F3C488};; +Protected Rapids;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9540CC1E-3E98-4C3D-A214-DC26A7DE6953};Saatavuutta rajoitettu;{9540CC1E-3E98-4C3D-A214-DC26A7DE6953};;;https://metadata.ymparisto.fi/dataset/;; +PUNALEVÄYHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={179A08D0-3B37-4A80-837E-83F151D5CDEA};Saatavuutta rajoitettu;{179A08D0-3B37-4A80-837E-83F151D5CDEA};;;https://metadata.ymparisto.fi/dataset/;; +Puolustusvoimien suoja-alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={61D3FA7B-D8AC-40BA-A552-9FCED8F87375};Saatavuutta rajoitettu;{61D3FA7B-D8AC-40BA-A552-9FCED8F87375};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +PYLAIELLA LITTORALIS & ECTOCARPUS SILICULOSUS LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CF4921DE-7CD1-48C6-8C9C-1BFFC6662E14};Saatavuutta rajoitettu;{CF4921DE-7CD1-48C6-8C9C-1BFFC6662E14};{955901B1-C6D0-45CA-8EA6-C1EF95CA1C50};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{955901B1-C6D0-45CA-8EA6-C1EF95CA1C50};; +Rakennusperinnön hoitoavustushakemukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={9D206B64-2EB7-47CC-AC9C-48459242444E};Saatavuutta rajoitettu;{9D206B64-2EB7-47CC-AC9C-48459242444E};{2CAEA52F-82FA-4E96-8E79-1327F27EF9A9};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2CAEA52F-82FA-4E96-8E79-1327F27EF9A9};; +Rakennusperinnön inventointi kohteet ja alueet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4B84256C-0F01-456E-8A41-63231FC0D8F5};Saatavuutta rajoitettu;{4B84256C-0F01-456E-8A41-63231FC0D8F5};{D653718F-EE09-4F08-95B5-2AC42BAD5337};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D653718F-EE09-4F08-95B5-2AC42BAD5337};; +Rantalohkojako (SÖKÖ-toimintamalli);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={11A39DCD-4A35-4B90-AFBD-EEC3B778378C};Saatavuutta rajoitettu;{11A39DCD-4A35-4B90-AFBD-EEC3B778378C};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +River Basin Districts;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={65C4C67D-94F6-4B52-A656-069CC5853778};Saatavuutta rajoitettu;{65C4C67D-94F6-4B52-A656-069CC5853778};;;https://metadata.ymparisto.fi/dataset/;; +Ruoppaukset rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1A50B09C-2DEF-46C0-8664-19A3A5BF89CE};Saatavuutta rajoitettu;{1A50B09C-2DEF-46C0-8664-19A3A5BF89CE};{911BBDBD-5371-44E5-8AEA-3DEE2248AEE6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{911BBDBD-5371-44E5-8AEA-3DEE2248AEE6};; +Saaristomeren ja Selkämeren fladat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={45E434FE-36DF-431C-A9F2-D65C433F8751};Saatavuutta rajoitettu;{45E434FE-36DF-431C-A9F2-D65C433F8751};{FC7C4F23-81E4-4037-9DED-E45575205D30};Turun ammattikorkeakoulu;https://metadata.ymparisto.fi/dataset/{FC7C4F23-81E4-4037-9DED-E45575205D30};; +Sea Region Division;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={313F5784-71A0-46C2-A5B2-86AD9B17A4E8};Saatavuutta rajoitettu;{313F5784-71A0-46C2-A5B2-86AD9B17A4E8};;;https://metadata.ymparisto.fi/dataset/;; +Shoreline10 (Ranta10) and River network;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CED8AC3C-0651-4117-88A6-44CD05F6B42D};Saatavuutta rajoitettu;{CED8AC3C-0651-4117-88A6-44CD05F6B42D};;;https://metadata.ymparisto.fi/dataset/;; +SINISIMPUKKAYHTEISÖJEN LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2F7CC7D6-380E-4E77-834F-7682EC6BC4FF};Saatavuutta rajoitettu;{2F7CC7D6-380E-4E77-834F-7682EC6BC4FF};;;https://metadata.ymparisto.fi/dataset/;; +STUCKENIA FILIFORMIS & STUCKENIA PECTINATA LEVINNEISYYSMALLI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BD151356-6EDB-4CB2-9BEF-E72D5AEBE936};Saatavuutta rajoitettu;{BD151356-6EDB-4CB2-9BEF-E72D5AEBE936};{BC774445-3D76-4F4F-B6A4-A63C631F0BAE};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BC774445-3D76-4F4F-B6A4-A63C631F0BAE};; +Sukelluksissa havaittujen makroleväsukujen otannalla korjattu määrä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FF890247-EDD4-45E2-97BE-4570039870B7};Saatavuutta rajoitettu;{FF890247-EDD4-45E2-97BE-4570039870B7};{FBE17630-F6E1-47EA-B07E-C4A29BC33E3D};Metsähallitus;https://metadata.ymparisto.fi/dataset/{FBE17630-F6E1-47EA-B07E-C4A29BC33E3D};; +SYKEn hakemistorajapinta (OData);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3563384E-DF21-49F6-9E0A-CCB1B8A74A44};Saatavuutta rajoitettu;{3563384E-DF21-49F6-9E0A-CCB1B8A74A44};{318D5E0E-7E13-47F2-93E7-04DFE2A12917};Suomen ympäristökeskus / Tietokeskus;https://metadata.ymparisto.fi/dataset/{318D5E0E-7E13-47F2-93E7-04DFE2A12917};; +Säähavaintoasemat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D2BF7AA2-6E0A-4EFF-BB77-1EA5330A3518};Saatavuutta rajoitettu;{D2BF7AA2-6E0A-4EFF-BB77-1EA5330A3518};{CDE7624A-AA3A-4592-B086-24008F05108D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{CDE7624A-AA3A-4592-B086-24008F05108D};; +SÖKÖ-karttalehdet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3A7B5ECF-5A1A-4CE8-8880-EFA161B0452E};Saatavuutta rajoitettu;{3A7B5ECF-5A1A-4CE8-8880-EFA161B0452E};{D925EC77-C5F0-4D1E-A691-93ED0FACDAD6};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D925EC77-C5F0-4D1E-A691-93ED0FACDAD6};; +Teollisuus rannikolla;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={BDBE0944-5B2C-4CE8-A404-34B8292DF808};Saatavuutta rajoitettu;{BDBE0944-5B2C-4CE8-A404-34B8292DF808};{8D033A6C-D9A7-4622-B518-E4358303BD2C};Pohjois-Pohjanmaan ELY;https://metadata.ymparisto.fi/dataset/{8D033A6C-D9A7-4622-B518-E4358303BD2C};; +Tieriskirekisteri TSRR;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8047A089-C13F-4729-920B-3C18F471F711};Saatavuutta rajoitettu;{8047A089-C13F-4729-920B-3C18F471F711};{47A9B147-CC81-460F-976B-E3A2A756D15A};Pohjavesiaineisto- SYKE, Tieaineisto- LiV;https://metadata.ymparisto.fi/dataset/{47A9B147-CC81-460F-976B-E3A2A756D15A};; +Total Suspended Matter, FI-LAKES, 2012-06-20;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=e98cabc7-65ad-4e72-a6d6-f42e2cc591da;Saatavuutta rajoitettu;e98cabc7-65ad-4e72-a6d6-f42e2cc591da;;;https://metadata.ymparisto.fi/dataset/;; +Total suspended matter, FI-SOUTH, 2009-09-15;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid=69b7df80-3d67-11e2-941e-005056c00008;Saatavuutta rajoitettu;69b7df80-3d67-11e2-941e-005056c00008;;;https://metadata.ymparisto.fi/dataset/;; +Traffic and Water Ways' Restriction Areas;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={A8126E3C-8F96-43C3-883D-BF315E38284E};Saatavuutta rajoitettu;{A8126E3C-8F96-43C3-883D-BF315E38284E};;;https://metadata.ymparisto.fi/dataset/;; +Traffic Risk Zones;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F1E7CB96-6CA3-4E51-B057-464AF658C46E};Saatavuutta rajoitettu;{F1E7CB96-6CA3-4E51-B057-464AF658C46E};;;https://metadata.ymparisto.fi/dataset/;; +Tulvariskiruudut;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7F0ABF2F-A175-4D9A-A04B-C6514B94D082};Saatavuutta rajoitettu;{7F0ABF2F-A175-4D9A-A04B-C6514B94D082};;Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/;; +Tulvatietojärjestelmä TULVATJ;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={58AAFEFC-1429-4343-8EE2-F5E136E0A276};Saatavuutta rajoitettu;{58AAFEFC-1429-4343-8EE2-F5E136E0A276};{58AAFEFC-1429-4343-8EE2-F5E136E0A276};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{58AAFEFC-1429-4343-8EE2-F5E136E0A276};; +Tuottajavastuutietojärjestelmä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={0E73BB28-1574-48A0-87CA-FD4BC1211896};Saatavuutta rajoitettu;{0E73BB28-1574-48A0-87CA-FD4BC1211896};;;https://metadata.ymparisto.fi/dataset/;; +Uomatietojärjestelmä;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FB936A19-421D-486D-95AC-009BD53AA1B2};Saatavuutta rajoitettu;{FB936A19-421D-486D-95AC-009BD53AA1B2};{FB936A19-421D-486D-95AC-009BD53AA1B2};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{FB936A19-421D-486D-95AC-009BD53AA1B2};; +Valtakunnallinen maakuntakaavapaikkatietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={8E578155-1FD3-4B69-B6EB-0AB18B8F6FE6};Saatavuutta rajoitettu;{8E578155-1FD3-4B69-B6EB-0AB18B8F6FE6};{82174E6B-DBC6-439F-A0F0-B425C8FBB28F};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{82174E6B-DBC6-439F-A0F0-B425C8FBB28F};; +Valtakunnallisen yöperhosseurannan keskustietokanta YÖPETI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1FF762A2-EE3F-415C-9895-D866B2E8A9CF};Saatavuutta rajoitettu;{1FF762A2-EE3F-415C-9895-D866B2E8A9CF};{4CA7BB48-01F0-4544-B153-E17569155FBD};SYKE;https://metadata.ymparisto.fi/dataset/{4CA7BB48-01F0-4544-B153-E17569155FBD};; +Valtion öljyntorjuntavarastot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3E35CBD1-1C57-4E17-96EC-6E41256E4619};Saatavuutta rajoitettu;{3E35CBD1-1C57-4E17-96EC-6E41256E4619};{872D9B86-1902-4343-999C-2F76348C4524};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{872D9B86-1902-4343-999C-2F76348C4524};; +Value-valuma-aluejako (ehdotus);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={31B11BD6-7886-447C-8A24-2B8FFE6B2D07};Saatavuutta rajoitettu;{31B11BD6-7886-447C-8A24-2B8FFE6B2D07};{E848DF66-C3A7-47E5-956A-5FEBBDBD924B};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{E848DF66-C3A7-47E5-956A-5FEBBDBD924B};; +Valvonta ja kuormitustietojärjestelmä VAHTI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D1539BE1-7A48-4CC8-AEC9-B32C7650A399};Saatavuutta rajoitettu;{D1539BE1-7A48-4CC8-AEC9-B32C7650A399};;;https://metadata.ymparisto.fi/dataset/;; +Vanhan rakennuslain mukaisesti vahvistettujen yleiskaavojen rasterit;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1377662D-F6F1-48F7-86A0-ADD001F08CE1};Saatavuutta rajoitettu;{1377662D-F6F1-48F7-86A0-ADD001F08CE1};{970599E7-8793-47A6-BF67-23E845E82ED1};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{970599E7-8793-47A6-BF67-23E845E82ED1};; +Vanhan rakennuslain mukaisesti vahvistettujen yleiskaavojen ulkorajaukset;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={1E200F2D-7983-42C1-84AF-459FD31AB4EC};Saatavuutta rajoitettu;{1E200F2D-7983-42C1-84AF-459FD31AB4EC};{AAFD1646-16FC-42D8-9F1B-DA8272A9D062};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{AAFD1646-16FC-42D8-9F1B-DA8272A9D062};; +Varsinais-Suomen veneluiskat 2016;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={AFB2C26E-E63B-4324-8E06-3B806EC37C92};Saatavuutta rajoitettu;{AFB2C26E-E63B-4324-8E06-3B806EC37C92};{5E45B067-25E6-41F2-8129-DD7C2BF67148};Turun ammattikorkeakoulu Oy;https://metadata.ymparisto.fi/dataset/{5E45B067-25E6-41F2-8129-DD7C2BF67148};; +Varsinais-Suomen väylien riskikartoitus;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={00DEEA25-2286-49B4-AED4-CB20A1BA5B9E};Saatavuutta rajoitettu;{00DEEA25-2286-49B4-AED4-CB20A1BA5B9E};{AB829531-5139-4A9D-9CF6-162AE62B27D0};Turun ammattikorkeakoulu;https://metadata.ymparisto.fi/dataset/{AB829531-5139-4A9D-9CF6-162AE62B27D0};; +VELMU kartoitusmenetelmät;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={7F62E2AC-7C1B-4B3F-A66A-8BE03E01388B};Saatavuutta rajoitettu;{7F62E2AC-7C1B-4B3F-A66A-8BE03E01388B};{2BED2EB3-CDAA-4071-ADD3-1FC081C23440};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{2BED2EB3-CDAA-4071-ADD3-1FC081C23440};; +VELMU kuvapisteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={FBE41DF9-F410-4490-B9B6-8418CB33617E};Saatavuutta rajoitettu;{FBE41DF9-F410-4490-B9B6-8418CB33617E};{4504165A-74A4-4675-B95A-FA5274C4984A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{4504165A-74A4-4675-B95A-FA5274C4984A};; +VELMU lajihavainnot;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={D03358DD-CA34-487F-8F85-03A74C9D7296};Saatavuutta rajoitettu;{D03358DD-CA34-487F-8F85-03A74C9D7296};{C3C7C925-5872-4C30-A824-8F723BE3ABC8};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{C3C7C925-5872-4C30-A824-8F723BE3ABC8};; +VELMU lajihavainnot (uhanalaiset lajit);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F500A951-A177-4CAA-83C5-FC54F2255FAB};Saatavuutta rajoitettu;{F500A951-A177-4CAA-83C5-FC54F2255FAB};{BF30DFF5-B5F8-4D8D-BAF7-D3671BBE432A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{BF30DFF5-B5F8-4D8D-BAF7-D3671BBE432A};; +Velmu taustakartta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B2B2A595-1225-4E18-B49C-EEBB5A6C5CE3};Saatavuutta rajoitettu;{B2B2A595-1225-4E18-B49C-EEBB5A6C5CE3};{60776441-5F9D-43EB-B902-A30CFBE89C2A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{60776441-5F9D-43EB-B902-A30CFBE89C2A};; +Venesatamat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={4166A97E-E513-4B69-B498-447A8FA71B9A};Saatavuutta rajoitettu;{4166A97E-E513-4B69-B498-447A8FA71B9A};{D4C89F91-AA18-4359-B1D6-42C9C436332A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{D4C89F91-AA18-4359-B1D6-42C9C436332A};; +Vesienhoitoalueiden pintavesien kaikki seurantapaikat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={356F0345-ABFE-4245-8231-BE4BFA83C541};Saatavuutta rajoitettu;{356F0345-ABFE-4245-8231-BE4BFA83C541};{79CF5A64-2690-4BF1-83C9-66C9CF8C041D};Suomen ympäristökeskus/Vesikeskus;https://metadata.ymparisto.fi/dataset/{79CF5A64-2690-4BF1-83C9-66C9CF8C041D};; +Vesienhoitoalueiden seuranta - VHS Seuranta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F2FAD430-3632-4ACC-B2B1-70680795361E};Saatavuutta rajoitettu;{F2FAD430-3632-4ACC-B2B1-70680795361E};{F2FAD430-3632-4ACC-B2B1-70680795361E};Suomen ympäristökeskus / Vesikeskus / Sisävesiyksikkö;https://metadata.ymparisto.fi/dataset/{F2FAD430-3632-4ACC-B2B1-70680795361E};; +vesienhoitoalueiden seurantapaikat, Pohjavesi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={3E07BDDC-AB68-4CDB-A647-BA6FCA784AB3};Saatavuutta rajoitettu;{3E07BDDC-AB68-4CDB-A647-BA6FCA784AB3};{5168AC0A-DD96-4231-9B4A-26DEAC055DC4};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5168AC0A-DD96-4231-9B4A-26DEAC055DC4};; +Vesienhoitoalueiden seurantapaikat. Pintavesi;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={22E6CAF3-20A9-44D1-B054-E019FEF882D4};Saatavuutta rajoitettu;{22E6CAF3-20A9-44D1-B054-E019FEF882D4};;;https://metadata.ymparisto.fi/dataset/;; +Vesihuollon tietojärjestelmä VEETI;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={F363ACA2-6CF5-46EF-907C-8F6CAF50C17B};Saatavuutta rajoitettu;{F363ACA2-6CF5-46EF-907C-8F6CAF50C17B};{373B7BEC-F23D-4AEF-9194-A646006F95AD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{373B7BEC-F23D-4AEF-9194-A646006F95AD};; +Vesihuoltolaitosten tilastointijärjestelmä VELVET;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={DA001EC6-DB26-4339-AB97-DAB8FC2968B3};Saatavuutta rajoitettu;{DA001EC6-DB26-4339-AB97-DAB8FC2968B3};;;https://metadata.ymparisto.fi/dataset/;; +Vesihuoltoverkoston potentiaali (jätevesi, talousvesi ja jätevesi);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={479C025B-3ECC-4E97-8FF0-23E82B5A521B};Saatavuutta rajoitettu;{479C025B-3ECC-4E97-8FF0-23E82B5A521B};{363584D7-C854-4A20-BD7E-A1385B4E171D};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{363584D7-C854-4A20-BD7E-A1385B4E171D};; +Vesiliikennerajoitukset-karttapalvelu;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2AD52986-F67E-4417-B733-BF57C94B4F02};Saatavuutta rajoitettu;{2AD52986-F67E-4417-B733-BF57C94B4F02};{5E05F2A7-17C5-44D3-99E2-68C263D3DD32};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{5E05F2A7-17C5-44D3-99E2-68C263D3DD32};; +Vesimuodostumat-tietojärjestelmä VEMU;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6CD780AB-790D-4DA8-8962-9C8186B4127D};Saatavuutta rajoitettu;{6CD780AB-790D-4DA8-8962-9C8186B4127D};{6CD780AB-790D-4DA8-8962-9C8186B4127D};Suomen ympäristökeskus / Vesikeskus / Vesivarayksikkö;https://metadata.ymparisto.fi/dataset/{6CD780AB-790D-4DA8-8962-9C8186B4127D};; +Vesistömallijärjestelmä (WSFS-VEMALA);http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={C08C0416-6605-4BC5-8956-58AAF95F0030};Saatavuutta rajoitettu;{C08C0416-6605-4BC5-8956-58AAF95F0030};;;https://metadata.ymparisto.fi/dataset/;; +Vesistötyöt VESTY - Vesistöhankkeet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CA4578AC-3B2E-488F-8A0D-C91E470DAE49};Saatavuutta rajoitettu;{CA4578AC-3B2E-488F-8A0D-C91E470DAE49};{789B889E-6D94-413F-BD07-F811137637B7};Lasse Järvenpää;https://metadata.ymparisto.fi/dataset/{789B889E-6D94-413F-BD07-F811137637B7};; +Vesistötyöt VESTY - Rakenteet ja toimenpiteet;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={54CB6655-3038-4FAC-9822-5BADA622570F};Saatavuutta rajoitettu;{54CB6655-3038-4FAC-9822-5BADA622570F};{C13F44E5-E626-4AE5-BD23-EDCC1A21CDA9};Suomen ympäristökeskus / Vesikeskus / Vesivarayksikkö;https://metadata.ymparisto.fi/dataset/{C13F44E5-E626-4AE5-BD23-EDCC1A21CDA9};; +Vesivarat - käyttötoiminta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={6F7ADB6E-193B-47A2-A4F6-2320D66D706A};Saatavuutta rajoitettu;{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{6F7ADB6E-193B-47A2-A4F6-2320D66D706A};; +Vesiviljelyn sijainninohjaussuunnitelma;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={E9B1F82F-D696-49A1-9CC8-B69EEE5D61FF};Saatavuutta rajoitettu;{E9B1F82F-D696-49A1-9CC8-B69EEE5D61FF};;Maa- ja metsätalousministeriö, Ruokavirasto;https://metadata.ymparisto.fi/dataset/;; +Väestötietojärjestelmän rakennus- ja huoneistotiedot RHR;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={18C8309E-3963-4738-B6D2-96BA825344DC};Saatavuutta rajoitettu;{18C8309E-3963-4738-B6D2-96BA825344DC};;Digi- ja väestötietovirasto;https://metadata.ymparisto.fi/dataset/;; +Vähittäiskaupan toimipaikat;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={2A5BD858-ECAB-4E8B-B3B1-7FD0FE0DA798};Saatavuutta rajoitettu;{2A5BD858-ECAB-4E8B-B3B1-7FD0FE0DA798};;Tilastokeskus;https://metadata.ymparisto.fi/dataset/;; +Yleiskaavarekisteri - Yleiskaava MOPO;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={34E2147E-D704-408D-A053-F91EEE20F94F};Saatavuutta rajoitettu;{34E2147E-D704-408D-A053-F91EEE20F94F};;;https://metadata.ymparisto.fi/dataset/;; +Ympäristöhallinnon kirjastojen kokoelma- ja artikkelitietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={51D1BC1D-93E5-4369-8019-630ACDEF53BA};Saatavuutta rajoitettu;{51D1BC1D-93E5-4369-8019-630ACDEF53BA};{51D1BC1D-93E5-4369-8019-630ACDEF53BA};Suomen ympäristökeskus / Tietokeskus / Tietopalvelu ja kirjasto;https://metadata.ymparisto.fi/dataset/{51D1BC1D-93E5-4369-8019-630ACDEF53BA};; +Ympäristöhallinnon kirjastojen lehtitietokanta;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B313B6FD-1CAB-41DD-9EF8-CF602F361773};Saatavuutta rajoitettu;{B313B6FD-1CAB-41DD-9EF8-CF602F361773};{B313B6FD-1CAB-41DD-9EF8-CF602F361773};Suomen ympäristökeskus / Tietokeskus / Tietopalvelu ja kirjasto;https://metadata.ymparisto.fi/dataset/{B313B6FD-1CAB-41DD-9EF8-CF602F361773};; +Ympäristökarttapalvelu karpalo;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={B7FB975C-846E-451A-9F5E-00EE3D2383AA};Saatavuutta rajoitettu;{B7FB975C-846E-451A-9F5E-00EE3D2383AA};{1334BB17-D27D-4876-9362-EF59256CC6BD};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{1334BB17-D27D-4876-9362-EF59256CC6BD};; Zostera marina levinneisyyysmalli;http://metatieto.ymparisto.fi:8080/geoportal/catalog/search/resource/details.page?uuid={CA32C9F7-7475-4EFE-8E1C-22B1FB96F008};Saatavuutta rajoitettu;{CA32C9F7-7475-4EFE-8E1C-22B1FB96F008};{30F193DB-2CD7-4DC1-8A31-84CB06B16213};Suomen ympäristökeskus;https://metadata.ymparisto.fi/dataset/{30F193DB-2CD7-4DC1-8A31-84CB06B16213};; \ No newline at end of file diff --git a/src/metax_api/models/__init__.py b/src/metax_api/models/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/models/catalog_record.py b/src/metax_api/models/catalog_record.py old mode 100644 new mode 100755 index 831e8320..36544d6f --- a/src/metax_api/models/catalog_record.py +++ b/src/metax_api/models/catalog_record.py @@ -5,9 +5,9 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import logging from collections import defaultdict from copy import deepcopy -import logging from django.conf import settings from django.contrib.postgres.fields import ArrayField @@ -26,7 +26,6 @@ from .directory import Directory from .file import File - READ_METHODS = ('GET', 'HEAD', 'OPTIONS') DEBUG = settings.DEBUG _logger = logging.getLogger(__name__) @@ -955,13 +954,14 @@ def delete(self, *args, **kwargs): self.previous_dataset_version.next_dataset_version = None super(Common, self.previous_dataset_version).save() - super(Common, self).delete() - return + crid = self.id + super().delete() + return crid elif self.state == self.STATE_PUBLISHED: if self.has_alternate_records(): self._remove_from_alternate_record_set() - if get_identifier_type(self.preferred_identifier) == IdentifierType.DOI: + if is_metax_generated_doi_identifier(self.research_dataset['preferred_identifier']): self.add_post_request_callable(DataciteDOIUpdate(self, self.research_dataset['preferred_identifier'], 'delete')) @@ -982,13 +982,16 @@ def delete(self, *args, **kwargs): } if self.catalog_is_legacy(): # delete permanently instead of only marking as 'removed' + crid = self.id super().delete() + return crid else: super().remove(*args, **kwargs) log_args['catalogrecord']['date_removed'] = datetime_to_str(self.date_removed) log_args['catalogrecord']['date_modified'] = datetime_to_str(self.date_modified) self.add_post_request_callable(DelayedLog(**log_args)) + return self.id def deprecate(self, timestamp=None): self.deprecated = True @@ -1254,6 +1257,7 @@ def _post_create_operations(self): if (get_identifier_type(self.preferred_identifier) == IdentifierType.DOI or self.use_doi_for_published is True): self._validate_cr_against_datacite_schema() + if is_metax_generated_doi_identifier(self.research_dataset['preferred_identifier']): self.add_post_request_callable(DataciteDOIUpdate(self, self.research_dataset['preferred_identifier'], 'create')) @@ -1459,8 +1463,9 @@ def _post_update_operations(self): if get_identifier_type(self.preferred_identifier) == IdentifierType.DOI and \ self.update_datacite: self._validate_cr_against_datacite_schema() - self.add_post_request_callable(DataciteDOIUpdate(self, self.research_dataset['preferred_identifier'], - 'update')) + if is_metax_generated_doi_identifier(self.research_dataset['preferred_identifier']): + self.add_post_request_callable(DataciteDOIUpdate(self, + self.research_dataset['preferred_identifier'], 'update')) if self.state == self.STATE_PUBLISHED: self.add_post_request_callable(RabbitMQPublishRecord(self, 'update')) @@ -2036,7 +2041,7 @@ def _create_new_dataset_version(self): new_version.calculate_directory_byte_sizes_and_file_counts() - if pref_id_type == IdentifierType.DOI: + if is_metax_generated_doi_identifier(self.research_dataset['preferred_identifier']): self.add_post_request_callable(DataciteDOIUpdate(new_version, new_version.research_dataset['preferred_identifier'], 'create')) @@ -2652,7 +2657,8 @@ def __call__(self): cr_json['data_catalog'] = {'catalog_json': self.cr.data_catalog.catalog_json} try: - rabbitmq.publish(cr_json, routing_key=self.routing_key, exchange='datasets') + for exchange in settings.RABBITMQ["EXCHANGES"]: + rabbitmq.publish(cr_json, routing_key=self.routing_key, exchange=exchange["NAME"]) except: # note: if we'd like to let the request be a success even if this operation fails, # we could simply not raise an exception here. diff --git a/src/metax_api/models/catalog_record_v2.py b/src/metax_api/models/catalog_record_v2.py old mode 100644 new mode 100755 index 1e9a9b11..5f25a135 --- a/src/metax_api/models/catalog_record_v2.py +++ b/src/metax_api/models/catalog_record_v2.py @@ -5,12 +5,13 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import logging from collections import defaultdict from copy import deepcopy -import logging from django.conf import settings from django.db.models import Q +from rest_framework.serializers import ValidationError from metax_api.exceptions import Http400, Http403 from metax_api.utils import ( @@ -22,16 +23,15 @@ get_tz_aware_now_without_micros, IdentifierType, ) -from .common import Common -from .directory import Directory -from .file import File from .catalog_record import ( CatalogRecord, DataciteDOIUpdate, DatasetVersionSet, RabbitMQPublishRecord, ) - +from .common import Common +from .directory import Directory +from .file import File _logger = logging.getLogger(__name__) @@ -158,6 +158,9 @@ def _post_create_operations(self, pid_type=None): def is_draft_for_another_dataset(self): return hasattr(self, 'draft_of') and self.draft_of is not None + def has_next_draft(self): + return hasattr(self, 'next_draft') and self.next_draft is not None + def _save_as_draft(self): """ Inherit here to always allow drafts in v2 api since the whole workflow is based on them @@ -283,6 +286,15 @@ def publish_dataset(self, pid_type=None): if self.api_version != self.api_meta['version']: self._set_api_version() + try: + # drafts are validated with different schema until the publication so check that final result is valid + # according to the actual data catalog. + serializer = self.serializer_class(self, context={'request': self.request}, data=self._initial_data) + serializer.validate_json_schema(self.research_dataset) + except ValidationError as e: + # apierrors couldn't handle the validation error thrown by the serializer + raise Http400(e) + super(Common, self).save() _logger.info( @@ -714,11 +726,8 @@ def change_files(self, file_changes, operation_is_create=False): _logger.debug('Received data does not include files or directories - returning') return - if self.catalog_is_dft(): - raise Http400('Adding files in draft catalog is not permitted. Please select valid datacatalog first.') - # create an instance of the serializer for later validations - serializer = self.serializer_class(self) + serializer = self.serializer_class(self, context={'request': self.request}, data=self._initial_data) if operation_is_create: # todo: probably it would be best to leave this timestamp field empty on initial create @@ -1214,6 +1223,11 @@ def create_new_version(self): 'Can\'t create new version. Dataset is a draft for another published dataset: %s' % self.draft_of.identifier ) + elif self.has_next_draft(): + raise Http400( + 'Can\'t create new version. Dataset has an unmerged draft: %s' + % self.next_draft.identifier + ) elif not self.catalog_versions_datasets(): raise Http400('Data catalog does not allow dataset versioning') elif self.state == self.STATE_DRAFT: diff --git a/src/metax_api/models/common.py b/src/metax_api/models/common.py old mode 100644 new mode 100755 diff --git a/src/metax_api/models/contract.py b/src/metax_api/models/contract.py old mode 100644 new mode 100755 index 56e35e5e..c5940eb3 --- a/src/metax_api/models/contract.py +++ b/src/metax_api/models/contract.py @@ -5,8 +5,8 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from django.db.models import JSONField from django.db import connection +from django.db.models import JSONField from .common import Common diff --git a/src/metax_api/models/data_catalog.py b/src/metax_api/models/data_catalog.py old mode 100644 new mode 100755 diff --git a/src/metax_api/models/directory.py b/src/metax_api/models/directory.py old mode 100644 new mode 100755 index f6684b46..331b92ad --- a/src/metax_api/models/directory.py +++ b/src/metax_api/models/directory.py @@ -8,13 +8,12 @@ import logging from django.db import connection, models -from django.db.models import Prefetch from django.db.models import Count, Sum +from django.db.models import Prefetch from .common import Common from .file import File - _logger = logging.getLogger(__name__) diff --git a/src/metax_api/models/file.py b/src/metax_api/models/file.py old mode 100644 new mode 100755 diff --git a/src/metax_api/models/file_storage.py b/src/metax_api/models/file_storage.py old mode 100644 new mode 100755 index 05cff344..1a1d5287 --- a/src/metax_api/models/file_storage.py +++ b/src/metax_api/models/file_storage.py @@ -6,6 +6,7 @@ # :license: MIT from django.db.models import JSONField + from .common import Common diff --git a/src/metax_api/models/metax_user.py b/src/metax_api/models/metax_user.py old mode 100644 new mode 100755 diff --git a/src/metax_api/models/xml_metadata.py b/src/metax_api/models/xml_metadata.py old mode 100644 new mode 100755 diff --git a/src/metax_api/onappstart.py b/src/metax_api/onappstart.py old mode 100644 new mode 100755 index d82db7ed..8ddd9fc0 --- a/src/metax_api/onappstart.py +++ b/src/metax_api/onappstart.py @@ -6,26 +6,27 @@ # :license: MIT import logging -import sys from os import makedirs, getpid from shutil import rmtree from time import sleep +from typing import Any from django.apps import AppConfig from django.conf import settings +from icecream import ic -from metax_api.utils import executing_test_case, json_logger, ReferenceDataLoader +from metax_api.utils import executing_test_case, ReferenceDataLoader _logger = logging.getLogger(__name__) class OnAppStart(AppConfig): - name = 'metax_api' + name = "metax_api" verbose_name = "Metax API" _pid = getpid() - def ready(self): # pragma: no cover + def ready(self): # pragma: no cover """ Execute various tasks during application startup: @@ -39,45 +40,59 @@ def ready(self): # pragma: no cover # some imports from metax_api cannot be done at the beginning of the file, # because the "django apps" have not been loaded yet. - from metax_api.services import RedisCacheService as cache, RabbitMQService as rabbitmq - - json_logger.info( - event='process_started', - process_id=self._pid + from metax_api.services import ( + RabbitMQService as rabbitmq, ) - - if not executing_test_case() and any(cmd in sys.argv for cmd in ['manage.py']): + from metax_api.services.redis_cache_service import RedisClient + from watchman.utils import get_checks + import json + + for check in get_checks(): + if callable(check): + resp: Any + try: + resp = json.dumps(check()) + ic(resp) + except TypeError as e: + e_resp = check() + _logger.error( + f"Error in system check: {e}, caused by check:{check.__name__} with return value of {e_resp}" + ) + _logger.info(f"event='process_started',process_id={self._pid}") + + """if not executing_test_case() and any(cmd in sys.argv for cmd in ['manage.py']): + _logger.info(f"process {self._pid} startapp task returned") return # ex = expiration in seconds if not cache.get_or_set('on_app_start_executing', True, ex=120): - return + _logger.info(f"process {self._pid} startapp tasks returned") + return""" # actual startup tasks -> - _logger.info('Metax API startup tasks executing...') + _logger.info("Metax API startup tasks executing...") + cache = RedisClient() try: - if executing_test_case(): - cache.get_master().flushdb() - if settings.ELASTICSEARCH['ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART']: - cache.set('reference_data', None) + if settings.ELASTICSEARCH["ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART"]: + cache.set("reference_data", None) - if not cache.get('reference_data', master=True) or not cache.get('ref_data_up_to_date', master=True): + if not cache.get("reference_data", master=True) or not cache.get( + "ref_data_up_to_date", master=True + ): ReferenceDataLoader.populate_cache_reference_data(cache) - json_logger.info( - event='reference_data_loaded', - process_id=self._pid - ) + _logger.info(f"event='reference_data_loaded',process_id={self._pid}") else: - pass - except: - raise + ic() + except Exception as e: + _logger.error(e) + # raise e finally: # ensure other processes have stopped at on_app_start_executing # before resetting the flag. (on local this method can be quite fast) sleep(2) - cache.delete('on_app_start_executing') + cache.delete("on_app_start_executing") if executing_test_case(): # reset error files location between tests @@ -94,4 +109,4 @@ def ready(self): # pragma: no cover _logger.error(e) _logger.error("Unable to initialize RabbitMQ exchanges") - _logger.info('Metax API startup tasks finished') + _logger.info("Metax API startup tasks finished") diff --git a/src/metax_api/parsers/__init__.py b/src/metax_api/parsers/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/parsers/parsers.py b/src/metax_api/parsers/parsers.py old mode 100644 new mode 100755 diff --git a/src/metax_api/permissions/__init__.py b/src/metax_api/permissions/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/permissions/permissions.py b/src/metax_api/permissions/permissions.py old mode 100644 new mode 100755 index ea719502..35a72ff4 --- a/src/metax_api/permissions/permissions.py +++ b/src/metax_api/permissions/permissions.py @@ -13,7 +13,6 @@ from metax_api.exceptions import Http400 - _logger = logging.getLogger(__name__) METHOD_MAP = { diff --git a/src/metax_api/renderers/__init__.py b/src/metax_api/renderers/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/renderers/renderers.py b/src/metax_api/renderers/renderers.py old mode 100644 new mode 100755 diff --git a/src/metax_api/services/__init__.py b/src/metax_api/services/__init__.py old mode 100644 new mode 100755 index 5a37ff7e..62ede701 --- a/src/metax_api/services/__init__.py +++ b/src/metax_api/services/__init__.py @@ -7,15 +7,16 @@ from .api_error_service import ApiErrorService from .auth_service import AuthService +from .callable_service import CallableService from .catalog_record_service import CatalogRecordService from .catalog_record_service_v2 import CatalogRecordServiceV2 -from .callable_service import CallableService from .common_service import CommonService from .data_catalog_service import DataCatalogService from .datacite_service import DataciteService from .file_service import FileService from .rabbitmq_service import RabbitMQService -from .redis_cache_service import RedisCacheService, _RedisCacheService, _RedisCacheServiceDummy +from .redis_cache_service import RedisClient as RedisCacheService, RedisClient as _RedisCacheService, \ + RedisClient as _RedisCacheServiceDummy from .reference_data_mixin import ReferenceDataMixin from .schema_service import SchemaService from .statistic_service import StatisticService diff --git a/src/metax_api/services/api_error_service.py b/src/metax_api/services/api_error_service.py old mode 100644 new mode 100755 index 5b251cd3..6b969b2a --- a/src/metax_api/services/api_error_service.py +++ b/src/metax_api/services/api_error_service.py @@ -5,11 +5,11 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import logging +import traceback from json import dump as json_dump, load as json_load from os import listdir, remove as remove_file from uuid import uuid4 -import logging -import traceback from django.conf import settings diff --git a/src/metax_api/services/auth_service.py b/src/metax_api/services/auth_service.py old mode 100644 new mode 100755 index 293ec998..795f1c48 --- a/src/metax_api/services/auth_service.py +++ b/src/metax_api/services/auth_service.py @@ -11,7 +11,6 @@ from django.conf import settings from django.http import Http404 - _logger = logging.getLogger(__name__) class AuthService(): @@ -84,7 +83,7 @@ def get_additional_user_projects_from_file(username): try: with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'r') as file: additional_projects = json.load(file) - except FileNotFoundError: + except FileNotFoundError: # noqa _logger.info("No local file for user projects") except Exception as e: _logger.error(e) diff --git a/src/metax_api/services/callable_service.py b/src/metax_api/services/callable_service.py old mode 100644 new mode 100755 diff --git a/src/metax_api/services/catalog_record_service.py b/src/metax_api/services/catalog_record_service.py old mode 100644 new mode 100755 index 29e0c87c..1738d398 --- a/src/metax_api/services/catalog_record_service.py +++ b/src/metax_api/services/catalog_record_service.py @@ -10,24 +10,25 @@ from os.path import dirname, join import simplexquery as sxq +import xmltodict from django.db.models import Q +from rest_framework import status +from rest_framework.response import Response from rest_framework.serializers import ValidationError -import xmltodict from metax_api.exceptions import Http400, Http403, Http503 from metax_api.models import CatalogRecord, Directory, File from metax_api.models.catalog_record import ACCESS_TYPES from metax_api.utils import \ - parse_timestamp_string_to_tz_aware_datetime,\ - get_tz_aware_now_without_micros,\ - remove_keys_recursively,\ + parse_timestamp_string_to_tz_aware_datetime, \ + get_tz_aware_now_without_micros, \ + remove_keys_recursively, \ leave_keys_in_dict from .common_service import CommonService from .datacite_service import DataciteService from .file_service import FileService from .reference_data_mixin import ReferenceDataMixin - _logger = logging.getLogger(__name__) @@ -404,6 +405,7 @@ def validate_reference_data(cls, research_dataset, cache): """ reference_data = cls.get_reference_data(cache) + # ic(reference_data) refdata = reference_data['reference_data'] orgdata = reference_data['organization_data']['organization'] errors = defaultdict(list) @@ -760,3 +762,33 @@ def get_research_dataset_license_url(rd): license = rd['access_rights']['license'][0] return license.get('identifier') or license.get('license') + + @classmethod + def destroy_bulk(cls, request): + """ + Mark datasets as deleted en masse. Parameter cr_identifiers can be a list of pk's + (integers), or file identifiers (strings). + """ + _logger.info('Begin bulk delete datasets') + + cr_ids = cls.identifiers_to_ids(request.data) + cr_deleted = [] + no_access = [] + for id in cr_ids: + try: + cr = CatalogRecord.objects.get(pk=id) + if cr.user_has_access(request): + cr_deleted.append(cr.delete()) + else: + no_access.append(id) + except: + pass + + if sorted(no_access) == sorted(cr_ids): + raise Http403({ 'detail': ['None of datasets exists or are permitted for users']}) + + if not cr_deleted: + return Response(cr_deleted, status=status.HTTP_404_NOT_FOUND) + + _logger.info(f'Marked datasets {cr_deleted} as deleted') + return Response(cr_deleted, status=status.HTTP_200_OK) diff --git a/src/metax_api/services/catalog_record_service_v2.py b/src/metax_api/services/catalog_record_service_v2.py old mode 100644 new mode 100755 index 0babbb47..92ca473f --- a/src/metax_api/services/catalog_record_service_v2.py +++ b/src/metax_api/services/catalog_record_service_v2.py @@ -8,7 +8,6 @@ from .catalog_record_service import CatalogRecordService - _logger = logging.getLogger(__name__) diff --git a/src/metax_api/services/common_service.py b/src/metax_api/services/common_service.py old mode 100644 new mode 100755 index 75ecbfb0..1eb87b55 --- a/src/metax_api/services/common_service.py +++ b/src/metax_api/services/common_service.py @@ -6,16 +6,19 @@ # :license: MIT import logging -from json import load as json_load +from typing import List +from json import load as json_load from django.db.models import Q from django.utils import timezone from rest_framework import status from rest_framework.request import Request from rest_framework.serializers import ValidationError + from metax_api.exceptions import Http400, Http412 -from metax_api.utils import parse_timestamp_string_to_tz_aware_datetime, get_tz_aware_now_without_micros from metax_api.models import File, CatalogRecord as cr +from metax_api.utils import parse_timestamp_string_to_tz_aware_datetime, get_tz_aware_now_without_micros + _logger = logging.getLogger(__name__) @@ -440,6 +443,16 @@ def _request_has_header(request, header_name): def _request_is_write_operation(request): return request.method in ('POST', 'PUT', 'PATCH', 'DELETE') + @staticmethod + def request_is_create_operation(request): + """ + This is fast'n dirty method for correctly choosing the json schema for dataset + validation. the if the path contains files, it means that the user is adding + files to the dataset thus, not creating the dataset. This might not work for other + datatypes out of the box. + """ + return request.method in ('POST') and 'files' not in request.path + @classmethod def check_if_unmodified_since(cls, request, obj): if cls._request_is_write_operation(request) and \ @@ -474,7 +487,7 @@ def set_if_modified_since_filter(cls, request, filter_obj): filter_obj['q_filters'] = [flter] @staticmethod - def identifiers_to_ids(identifiers, params=None): + def identifiers_to_ids(identifiers: List[any], params=None): """ In case identifiers are identifiers (strings), which they probably are in real use, do a query to get a list of pk's instead, since they will be used quite a few times. diff --git a/src/metax_api/services/data_catalog_service.py b/src/metax_api/services/data_catalog_service.py old mode 100644 new mode 100755 index 47f43d5e..5cecaa7e --- a/src/metax_api/services/data_catalog_service.py +++ b/src/metax_api/services/data_catalog_service.py @@ -7,10 +7,11 @@ from collections import defaultdict +# from icecream import ic from rest_framework.serializers import ValidationError -from .reference_data_mixin import ReferenceDataMixin from metax_api.models import DataCatalog +from .reference_data_mixin import ReferenceDataMixin class DataCatalogService(ReferenceDataMixin): @@ -30,8 +31,11 @@ def validate_reference_data(cls, data_catalog, cache): """ reference_data = cls.get_reference_data(cache) + refdata = reference_data['reference_data'] + # ic(refdata.keys()) orgdata = reference_data['organization_data'] + # ic(orgdata.keys()) errors = defaultdict(list) for language in data_catalog.get('language', []): diff --git a/src/metax_api/services/datacite_service.py b/src/metax_api/services/datacite_service.py old mode 100644 new mode 100755 index df291c65..52bd3c8c --- a/src/metax_api/services/datacite_service.py +++ b/src/metax_api/services/datacite_service.py @@ -14,10 +14,9 @@ from django.conf import settings as django_settings from metax_api.utils import extract_doi_from_doi_identifier, is_metax_generated_doi_identifier, executing_travis, \ - executing_test_case, is_metax_generated_urn_identifier, datetime_to_str + executing_test_case, is_metax_generated_urn_identifier, datetime_to_str, is_remote_doi_identifier from .common_service import CommonService - _logger = logging.getLogger(__name__) @@ -184,8 +183,9 @@ def get_validated_datacite_json(self, cr_json, is_strict, dummy_doi=False): identifier = cr_json.get('preservation_identifier', None) or pref_id is_metax_doi = is_metax_generated_doi_identifier(identifier) is_metax_urn = is_metax_generated_urn_identifier(identifier) + is_remote_doi = is_remote_doi_identifier(identifier) - if is_metax_doi: + if is_metax_doi or is_remote_doi: identifier_value = extract_doi_from_doi_identifier(identifier) identifier_type = 'DOI' elif dummy_doi: diff --git a/src/metax_api/services/file_service.py b/src/metax_api/services/file_service.py old mode 100644 new mode 100755 index 29da63aa..44e1c5c4 --- a/src/metax_api/services/file_service.py +++ b/src/metax_api/services/file_service.py @@ -5,8 +5,8 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from collections import defaultdict import logging +from collections import defaultdict from os import getpid from os.path import dirname, basename from time import time @@ -28,7 +28,6 @@ from .common_service import CommonService from .reference_data_mixin import ReferenceDataMixin - DEBUG = settings.DEBUG _logger = logging.getLogger(__name__) @@ -1252,7 +1251,7 @@ def _check_errors_before_creating_dirs(initial_data_list): but doing the minimum necessary here anyway since we can't count on FileSerializer.is_valid() """ for row in initial_data_list: - + # ic(row) if 'file_path' not in row: raise Http400({ 'file_path': ['file_path is a required parameter (file id: %s)' % row['identifier']] diff --git a/src/metax_api/services/pagination.py b/src/metax_api/services/pagination.py old mode 100644 new mode 100755 diff --git a/src/metax_api/services/rabbitmq_service.py b/src/metax_api/services/rabbitmq_service.py old mode 100644 new mode 100755 index a0a40654..aa44e49f --- a/src/metax_api/services/rabbitmq_service.py +++ b/src/metax_api/services/rabbitmq_service.py @@ -6,27 +6,27 @@ # :license: MIT import logging -from json import dumps as json_dumps import random +from json import dumps as json_dumps from time import sleep -from django.conf import settings import pika -from django.conf import settings as django_settings +from django.conf import settings from django.core.serializers.json import DjangoJSONEncoder from metax_api.utils.utils import executing_test_case, executing_travis _logger = logging.getLogger(__name__) -class _RabbitMQService(): - +class _RabbitMQService: def __init__(self): - if not hasattr(django_settings, 'RABBITMQ'): - raise Exception('Missing configuration from settings.py: RABBITMQ') - self._settings = django_settings.RABBITMQ - self._credentials = pika.PlainCredentials(self._settings['USER'], self._settings['PASSWORD']) - self._hosts = self._settings['HOSTS'] + if not hasattr(settings, "RABBITMQ"): + raise Exception("Missing configuration from settings.py: RABBITMQ") + self._settings = settings.RABBITMQ + self._credentials = pika.PlainCredentials( + self._settings["USER"], self._settings["PASSWORD"] + ) + self._hosts = self._settings["HOSTS"] self._connection = None def _connect(self): @@ -34,96 +34,38 @@ def _connect(self): return # Connection retries are needed as long as there is no load balancer in front of rabbitmq-server VMs - sleep_time = 2 - num_conn_retries = 15 + sleep_time = 1 + num_conn_retries = 5 + _logger.info( + f"connecting to RabbitMQ host: {self._hosts} port: {self._settings['PORT']}" + ) for x in range(0, num_conn_retries): # Choose host randomly so that different hosts are tried out in case of connection problems host = random.choice(self._hosts) try: - self._connection = pika.BlockingConnection(pika.ConnectionParameters( - host, - self._settings['PORT'], - self._settings['VHOST'], - self._credentials)) + kwarg_params = {"port": self._settings["PORT"], "credentials": self._credentials} + if settings.RABBIT_MQ_USE_VHOST: + kwarg_params["virtual_host"] = self._settings["VHOST"] + + conn_params = pika.ConnectionParameters(host, **kwarg_params) + self._connection = pika.BlockingConnection( + conn_params + ) + except Exception as e: - _logger.error("Problem connecting to RabbitMQ server (%s), trying to reconnect..." % str(e)) + _logger.error( + "Problem connecting to RabbitMQ server (%s), trying to reconnect..." + % str(e) + ) sleep(sleep_time) else: self._channel = self._connection.channel() - _logger.debug('RabbitMQ connected to %s' % host) + _logger.info("RabbitMQ connected to %s" % host) break else: raise Exception("Unable to connect to RabbitMQ") - def publish_to_TTV(self, body, routing_key='', exchange=None, persistent=True): - """ - Publish a message to an exchange, which might or might not have queues bound to it. - - body: body of the message. can be a list of messages, in which case each message is published - individually. - exchange: exchange to publish in - persistent: make message persist in rabbitmq storage over rabbitmq-server restart. - otherwise messages not retrieved by clients before restart will be lost. - (still is not 100 % guaranteed to persist!) - """ - # For testing - # credentials = pika.PlainCredentials(self._settings['USER'], self._settings['PASSWORD']) - # connection = pika.BlockingConnection(pika.ConnectionParameters( - # host = 'localhost', - # virtual_host = self._settings['VHOST'], - # port = 5672, - # credentials = credentials)) - - if settings.RABBITMQ_FOR_TTV_ENABLED: - # Choose host randomly so that different hosts are tried out in case of connection problems - host = random.choice(self._hosts) - connection = pika.BlockingConnection(pika.ConnectionParameters( - host, - self._settings['PORT'], - self._settings['VHOST_TTV'], - self._credentials)) - - if isinstance(body, list): - messages = body - else: - messages = [body] - - additional_args = {} - if persistent: - additional_args['properties'] = pika.BasicProperties(delivery_mode=2) - - channel = connection.channel() - - exchange = 'TTV-datasets' - queue_4 = 'ttv-create' - queue_5 = 'ttv-update' - queue_6 = 'ttv-delete' - - channel.exchange_declare(exchange=exchange, exchange_type='fanout') - channel.queue_declare(queue_4, durable=True) - channel.queue_declare(queue_5, durable=True) - channel.queue_declare(queue_6, durable=True) - - channel.queue_bind(exchange=exchange, queue=queue_4, routing_key='create') - channel.queue_bind(exchange=exchange, queue=queue_5, routing_key='update') - channel.queue_bind(exchange=exchange, queue=queue_6, routing_key='delete') - - try: - for message in messages: - if isinstance(message, dict): - message = json_dumps( - message, - cls=DjangoJSONEncoder) - channel.basic_publish(body=message, routing_key=routing_key, exchange=exchange, **additional_args) - except Exception as e: - _logger.error(e) - _logger.error("Unable to publish message to RabbitMQ") - raise - finally: - # for testing - connection.close() - def publish(self, body, routing_key='', exchange=None, persistent=True): """ Publish a message to an exchange, which might or might not have queues bound to it. @@ -143,7 +85,7 @@ def publish(self, body, routing_key='', exchange=None, persistent=True): additional_args = {} if persistent: - additional_args['properties'] = pika.BasicProperties(delivery_mode=2) + additional_args["properties"] = pika.BasicProperties(delivery_mode=2) if isinstance(body, list): messages = body @@ -157,7 +99,6 @@ def publish(self, body, routing_key='', exchange=None, persistent=True): message, cls=DjangoJSONEncoder) self._channel.basic_publish(body=message, routing_key=routing_key, exchange=exchange, **additional_args) - self.publish_to_TTV(body=message, routing_key=routing_key, exchange=None) except Exception as e: _logger.error(e) _logger.error("Unable to publish message to RabbitMQ") @@ -173,12 +114,23 @@ def init_exchanges(self): """ self._connect() try: - for exchange in self._settings['EXCHANGES']: + for exchange in self._settings["EXCHANGES"]: self._channel.exchange_declare( - exchange['NAME'], exchange_type=exchange['TYPE'], durable=exchange.get('DURABLE', True)) + exchange["NAME"], + exchange_type=exchange["TYPE"], + durable=exchange["DURABLE"], + ) + for queue in exchange.get("QUEUES", []): + # declare queues in settings + self._channel.queue_declare(queue["NAME"], durable=exchange["DURABLE"]) + self._channel.queue_bind( + queue["NAME"], + exchange["NAME"], + queue.get("ROUTING_KEY") + ) except Exception as e: _logger.error(e) - _logger.exception('Failed to initialize RabbitMQ exchanges') + _logger.exception("Failed to initialize RabbitMQ exchanges") raise finally: self._connection.close() @@ -190,23 +142,29 @@ def _validate_publish_params(self, routing_key, exchange_name): - routing_key is specified when exchange type is direct """ if not exchange_name: - raise Exception('Specify exchange to publish message to') + raise Exception("Specify exchange to publish message to") - for exchange in self._settings['EXCHANGES']: - if exchange_name == exchange['NAME'] and exchange['TYPE'] == 'direct' and routing_key == '': - raise Exception('Messages without routing_key are discarded when exchange type is \'direct\'') + for exchange in self._settings["EXCHANGES"]: + if ( + exchange_name == exchange["NAME"] + and exchange["TYPE"] == "direct" + and routing_key == "" + ): + raise Exception( + "Messages without routing_key are discarded when exchange type is 'direct'" + ) -class _RabbitMQServiceDummy(): +class _RabbitMQServiceDummy: """ A dummy rabbitmq client that doesn't connect anywhere and doesn't do jack actually. """ - def __init__(self, settings=django_settings): + def __init__(self, settings=settings): pass - def publish(self, body, routing_key='', exchange='datasets', persistent=True): + def publish(self, body, routing_key="", exchange="datasets", persistent=True): pass def init_exchanges(self, *args, **kwargs): diff --git a/src/metax_api/services/redis_cache_service.py b/src/metax_api/services/redis_cache_service.py old mode 100644 new mode 100755 index c1540dc4..a971a5ec --- a/src/metax_api/services/redis_cache_service.py +++ b/src/metax_api/services/redis_cache_service.py @@ -9,8 +9,10 @@ from json import dump as dump_json, load as load_json from pickle import dumps as pickle_dumps, loads as pickle_loads from random import choice as random_choice +from typing import Any -from django.conf import settings as django_settings +import redis +from django.conf import settings as django_settings, settings from redis.client import StrictRedis from redis.exceptions import TimeoutError, ConnectionError from redis.sentinel import MasterNotFoundError @@ -22,55 +24,115 @@ d = logging.getLogger(__name__).debug -class _RedisCacheService(): +class RedisClient(object): + def __init__(self, db=0): + if settings.REDIS_USE_PASSWORD is True: + self.client = redis.Redis( + password=settings.REDIS["PASSWORD"], + retry_on_timeout=True, + host=settings.REDIS["HOST"], + port=settings.REDIS["PORT"], + ) + else: + self.client = redis.Redis( + retry_on_timeout=True, + host=settings.REDIS["HOST"], + port=settings.REDIS["PORT"], + ) + _logger.info( + f"RedisClient created with host:{settings.REDIS['HOST']} port:{settings.REDIS['PORT']}" + ) + + def set(self, key, value, **kwargs): + pickled_data = pickle_dumps(value) + return self.client.set(key, pickled_data, **kwargs) + + def get_or_set(self, key, value, **kwargs): + """ + Atomic set of value only if key did not exist yet. + + Returns True if set was successful, None if set failed (= value existed) + + https://redis.io/commands/setnx Not recommended any longer for distributed locks... + https://redis.io/topics/distlock However this is also just a proposal and no official + implementation exists yet + """ + return self.set(key, value, nx=True, **kwargs) + + def get(self, key, master=False, **kwargs): + value: Any + try: + value = self.client.get(key) + except KeyError as e: + _logger.error(f"Redis has no {key} as key: {e}") + return pickle_loads(value) if value is not None else None + + def delete(self, *keys): + self.client.delete(*keys) + + def get_master(self): + """ + Expose the master node to permit any operation in redis-py + """ + return self.client + +class _RedisCacheService: def __init__(self, db=0): """ db: database index to read/write to. available indexes 0-15. """ - if hasattr(django_settings, 'REDIS'): + if hasattr(django_settings, "REDIS"): settings = django_settings.REDIS else: - raise Exception('Missing configuration from settings.py: REDIS') - - if not settings.get('SENTINEL', None): - raise Exception('Missing configuration from settings for REDIS: SENTINEL') - if not settings['SENTINEL'].get('HOSTS', None): - raise Exception('Missing configuration from settings for REDIS.SENTINEL: HOSTS') - if not settings['SENTINEL'].get('SERVICE', None): - raise Exception('Missing configuration from settings for REDIS.SENTINEL: SERVICE') - if not settings.get('TEST_DB', None): - raise Exception('Missing configuration from settings for REDIS: TEST_DB') - if len(settings['SENTINEL']['HOSTS']) < 3: - raise Exception('Invalid configuration in settings for REDIS.SENTINEL: HOSTS minimum number of hosts is 3') + raise Exception("Missing configuration from settings.py: REDIS") + + if not settings.get("SENTINEL", None): + raise Exception("Missing configuration from settings for REDIS: SENTINEL") + if not settings["SENTINEL"].get("HOSTS", None): + raise Exception( + "Missing configuration from settings for REDIS.SENTINEL: HOSTS" + ) + if not settings["SENTINEL"].get("SERVICE", None): + raise Exception( + "Missing configuration from settings for REDIS.SENTINEL: SERVICE" + ) + if not settings.get("TEST_DB", None): + raise Exception("Missing configuration from settings for REDIS: TEST_DB") + if len(settings["SENTINEL"]["HOSTS"]) < 3: + raise Exception( + "Invalid configuration in settings for REDIS.SENTINEL: HOSTS minimum number of hosts is 3" + ) if executing_test_case(): - db = settings['TEST_DB'] - elif db == settings['TEST_DB']: - raise Exception('Invalid db: db index %d is reserved for test suite execution.' % db) + db = settings["TEST_DB"] + elif db == settings["TEST_DB"]: + raise Exception( + "Invalid db: db index %d is reserved for test suite execution." % db + ) self._redis_local = StrictRedis( - host='localhost', - port=settings['LOCALHOST_PORT'], - password=settings['PASSWORD'], - socket_timeout=settings.get('SOCKET_TIMEOUT', 0.1), - db=db + host="localhost", + port=settings["LOCALHOST_PORT"], + password=settings["PASSWORD"], + socket_timeout=settings.get("SOCKET_TIMEOUT", 0.1), + db=db, ) self._sentinel = Sentinel( - settings['SENTINEL']['HOSTS'], - password=settings['PASSWORD'], - socket_timeout=settings.get('SOCKET_TIMEOUT', 0.1), - db=db + settings["SENTINEL"]["HOSTS"], + password=settings["PASSWORD"], + socket_timeout=settings.get("SOCKET_TIMEOUT", 0.1), + db=db, ) - self._service_name = settings['SENTINEL']['SERVICE'] - self._DEBUG = settings.get('DEBUG', False) + self._service_name = settings["SENTINEL"]["SERVICE"] + self._DEBUG = settings.get("DEBUG", False) self._node_count = self._count_nodes() def set(self, key, value, **kwargs): if self._DEBUG: - d('cache: set()...') + d("cache: set()...") pickled_data = pickle_dumps(value) master = self._get_master() @@ -79,18 +141,13 @@ def set(self, key, value, **kwargs): return master.set(key, pickled_data, **kwargs) except (TimeoutError, ConnectionError, MasterNotFoundError) as e: if self._DEBUG: - d('cache: master timed out or not found, or connection refused. no write instances available. error: %s' - % str(e)) + d( + f"cache: master timed out or not found, or connection refused. \ + No write instances available. error: {str(e)}" + ) # no master available return - if self._DEBUG: - test = master.get(key) - if test: - d('cache: set() successful') - else: - d('cache: set() unsuccessful, could not get saved data?') - def get_or_set(self, key, value, **kwargs): """ Atomic set of value only if key did not exist yet. @@ -114,7 +171,7 @@ def get(self, key, master=False, **kwargs): master=True to this method for a single get operation. """ if self._DEBUG: - d('cache: get()...') + d("cache: get()...") if master: return self._get_from_master(key, **kwargs) @@ -133,7 +190,7 @@ def get(self, key, master=False, **kwargs): def delete(self, *keys): if self._DEBUG: - d('cache: delete()...') + d("cache: delete()...") master = self._get_master() @@ -141,25 +198,29 @@ def delete(self, *keys): master.delete(*keys) except (TimeoutError, ConnectionError, MasterNotFoundError) as e: if self._DEBUG: - d('cache: master timed out or not found, or connection refused. no write instances available. error: %s' - % str(e)) + d( + f"cache: master timed out or not found, or connection refused. \ + No write instances available. error: {str(e)}" + ) # no master available return if self._DEBUG: test = master.get(keys[0]) if not test: - d('cache: delete() successful') + d("cache: delete() successful") else: - d('cache: delete() unsuccessful, could not delete data?') + d("cache: delete() unsuccessful, could not delete data?") def _get_from_local(self, key, **kwargs): try: res = self._redis_local.get(key, **kwargs) except (TimeoutError, ConnectionError): if self._DEBUG: - d('cache: _redis_local.get() timed out or connection refused, ' - 'trying from other slaves instead. fail-over in process?') + d( + "cache: _redis_local.get() timed out or connection refused, " + "trying from other slaves instead. fail-over in process?" + ) raise else: return pickle_loads(res) if res is not None else None @@ -170,8 +231,10 @@ def _get_from_slave(self, key, **kwargs): res = node.get(key, **kwargs) except (TimeoutError, ConnectionError): if self._DEBUG: - d('cache: slave.get() timed out or connection refused, ' - 'trying from master instead. fail-over in process?') + d( + "cache: slave.get() timed out or connection refused, " + "trying from master instead. fail-over in process?" + ) # fail-over propbably happened, and the old slave is now a master # (in case there was only one slave). try master instead raise @@ -184,7 +247,9 @@ def _get_from_master(self, key, **kwargs): res = master.get(key, **kwargs) except (TimeoutError, MasterNotFoundError): if self._DEBUG: - d('cache: master timed out also. no read instances available. returning None') + d( + "cache: master timed out also. no read instances available. returning None" + ) # uh oh, no master available either. either all redis instances have hit the bucket, # or there is a fail-over in process, and a new master will be in line in a moment return None @@ -201,25 +266,27 @@ def _slave_chosen(self): def _get_master(self): if self._DEBUG: - d('cache: getting master') + d("cache: getting master") return self._sentinel.master_for(self._service_name, socket_timeout=0.1) def _get_slave(self): if self._DEBUG: - d('cache: getting slave') + d("cache: getting slave") return self._sentinel.slave_for(self._service_name, socket_timeout=0.1) def _count_nodes(self): - return len(self._sentinel.discover_slaves(self._service_name)) + 1 # +1 is master + return ( + len(self._sentinel.discover_slaves(self._service_name)) + 1 + ) # +1 is master -class _RedisCacheServiceDummy(): +class _RedisCacheServiceDummy: """ A dummy redis client that writes to a file on disk. """ - _storage_path = '/tmp/redis_dummy_storage' + _storage_path = "/tmp/redis_dummy_storage" def __init__(self, *args, **kwargs): # d('Note: using dummy cache') @@ -237,7 +304,10 @@ def get_or_set(self, key, value, **kwargs): if self.get(key): return False else: - self.set(key, value,) + self.set( + key, + value, + ) return True def delete(self, key, **kwargs): @@ -254,27 +324,36 @@ def flushdb(self): def _get_storage(self): try: - with open(self._storage_path, 'r') as f: + with open(self._storage_path, "r") as f: return load_json(f) except IOError: self._save_storage({}) try: - with open(self._storage_path, 'r') as f: + with open(self._storage_path, "r") as f: return load_json(f) except Exception as e: - _logger.error('Could not open dummy cache file for reading at %s: %s' % (self._storage_path, str(e))) + _logger.error( + "Could not open dummy cache file for reading at %s: %s" + % (self._storage_path, str(e)) + ) except Exception as e: - _logger.error('Could not open dummy cache file for reading at %s: %s' % (self._storage_path, str(e))) + _logger.error( + "Could not open dummy cache file for reading at %s: %s" + % (self._storage_path, str(e)) + ) def _save_storage(self, storage): try: - with open(self._storage_path, 'w') as f: + with open(self._storage_path, "w") as f: dump_json(storage, f) except Exception as e: - _logger.error('Could not open dummy cache file for writing at %s: %s' % (self._storage_path, str(e))) + _logger.error( + "Could not open dummy cache file for writing at %s: %s" + % (self._storage_path, str(e)) + ) if executing_travis(): - RedisCacheService = _RedisCacheServiceDummy() + RedisCacheService = RedisClient else: - RedisCacheService = _RedisCacheService() + RedisCacheService = RedisClient diff --git a/src/metax_api/services/reference_data_mixin.py b/src/metax_api/services/reference_data_mixin.py old mode 100644 new mode 100755 index c7725c77..82bb3bbe --- a/src/metax_api/services/reference_data_mixin.py +++ b/src/metax_api/services/reference_data_mixin.py @@ -12,6 +12,7 @@ from metax_api.exceptions import Http503 from metax_api.utils import ReferenceDataLoader +from .redis_cache_service import RedisClient _logger = logging.getLogger(__name__) d = _logger.debug @@ -47,6 +48,7 @@ def check_ref_data(ref_data_type, field_to_check, relation_name, errors={}, valu return next(entry for entry in ref_data_type if field_to_check in (entry['uri'], entry['code'])) except StopIteration: if value_not_found_is_error: + _logger.error('Identifier \'%s\' not found in reference data' % field_to_check) errors[relation_name].append('Identifier \'%s\' not found in reference data' % field_to_check) return None @@ -61,10 +63,12 @@ def get_reference_data(cls, cache): need to be reloaded from the distributed cache again. Reference data does not change actively during normal operation, so saving it in the process should be safe. """ + cache = RedisClient() if cls.process_cached_reference_data is not None: return cls.process_cached_reference_data ref_data = cache.get('reference_data') + # ref_data += cache.get('reference-data') if ref_data: cls.process_cached_reference_data = ref_data diff --git a/src/metax_api/services/rems_service.py b/src/metax_api/services/rems_service.py old mode 100644 new mode 100755 index 1ebdbbf5..9cd7f9a4 --- a/src/metax_api/services/rems_service.py +++ b/src/metax_api/services/rems_service.py @@ -5,11 +5,10 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT import logging -import requests +import requests from django.conf import settings as django_settings - _logger = logging.getLogger(__name__) HANDLER_CLOSEABLE_APPLICATIONS = [ diff --git a/src/metax_api/services/schema_service.py b/src/metax_api/services/schema_service.py old mode 100644 new mode 100755 index 087a9aa5..92689f60 --- a/src/metax_api/services/schema_service.py +++ b/src/metax_api/services/schema_service.py @@ -6,15 +6,14 @@ # :license: MIT import logging - from os import listdir from os.path import isfile, join, abspath, dirname +from django.http import Http404 from rest_framework import status from rest_framework.response import Response -from django.http import Http404 -from metax_api.services import CommonService as CS +from metax_api.services import CommonService as CS _logger = logging.getLogger(__name__) d = logging.getLogger(__name__).debug diff --git a/src/metax_api/services/statistic_service.py b/src/metax_api/services/statistic_service.py old mode 100644 new mode 100755 index 09a71c34..21afeb8a --- a/src/metax_api/services/statistic_service.py +++ b/src/metax_api/services/statistic_service.py @@ -13,7 +13,6 @@ from metax_api.exceptions import Http400 from metax_api.models import CatalogRecord, DataCatalog - _logger = logging.getLogger(__name__) diff --git a/src/metax_api/settings.py b/src/metax_api/settings.py deleted file mode 100755 index 3b44a23c..00000000 --- a/src/metax_api/settings.py +++ /dev/null @@ -1,654 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -""" -Django settings for metax_api project. - -Generated by 'django-admin startproject' using Django 1.11. - -For more information on this file, see -https://docs.djangoproject.com/en/1.11/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.11/ref/settings/ -""" - -import logging.config -import os -import time - -import structlog -import yaml - -from metax_api.utils import executing_test_case, executing_travis - -executing_in_travis = executing_travis() -executing_in_test_case = executing_test_case() - -if not executing_in_travis: - with open('/home/metax-user/app_config') as app_config: - app_config_dict = yaml.load(app_config, Loader=yaml.FullLoader) - - if 'METAX_ENV' in app_config_dict: - METAX_ENV = app_config_dict['METAX_ENV'] - else: - raise Exception('METAX_ENV missing from app_config') - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -if executing_in_travis: - SECRET_KEY = '^pqn=v2i)%!w1oh=r!m_=wo_#w3)(@-#8%q_8&9z@slu+#q3+b' -else: - SECRET_KEY = app_config_dict['DJANGO_SECRET_KEY'] - -if executing_in_test_case or executing_in_travis: - # used by test cases and travis during test case execution to authenticate with certain api's - API_TEST_USER = { - 'username': 'testuser', - 'password': 'testuserpassword' - } - API_METAX_USER = { - 'username': 'metax', - 'password': 'metaxpassword' - } - API_AUTH_TEST_USER = { - 'username': 'api_auth_user', - 'password': 'password' - } - - API_EXT_USER = { - 'username': 'external', - 'password': 'externalpassword' - } - - API_TEST_USERS = [ - API_TEST_USER, - API_METAX_USER, - API_AUTH_TEST_USER, - API_EXT_USER - ] - - API_ACCESS = { - "rest": { - "apierrors": { - "read": ["testuser", "metax"], - "create": ["testuser", "metax"], - "update": ["testuser", "metax"], - "delete": ["testuser", "metax"] - }, - "contracts": { - "read": ["testuser", "metax"], - "create": ["testuser", "metax"], - "update": ["testuser", "metax"], - "delete": ["testuser", "metax"] - }, - "datacatalogs": { - "read": ["all"], - "create": ["testuser", "metax"], - "update": ["testuser", "metax"], - "delete": ["testuser", "metax"] - }, - "datasets": { - "read": ["all"], - "create": ["testuser", "metax", "api_auth_user", "endusers", "external"], - "update": ["testuser", "metax", "api_auth_user", "endusers", "external"], - "delete": ["testuser", "metax", "api_auth_user", "endusers", "external"] - }, - "directories": { - "read": ["testuser", "metax", "endusers"], - }, - "files": { - "read": ["testuser", "metax", "api_auth_user", "endusers"], - "create": ["testuser", "metax"], - "update": ["testuser", "metax", "endusers"], - "delete": ["testuser", "metax"] - }, - "filestorages": { - "read": ["testuser", "metax"], - "create": ["testuser", "metax"], - "update": ["testuser", "metax"], - "delete": ["testuser", "metax"] - }, - "schemas": { - "read": ["all"], - } - }, - "rpc": { - "datasets": { - "change_cumulative_state": { "use": ["all"] }, - "get_minimal_dataset_template": { "use": ["all"] }, - "refresh_directory_content": { "use": ["all"]}, - "fix_deprecated": { "use": ["all"] }, - "set_preservation_identifier": { "use": ["metax", "tpas"] }, - "create_new_version": { "use": ["all"] }, - "publish_dataset": { "use": ["all"] }, - "create_draft": { "use": ["all"] }, - "merge_draft": { "use": ["all"] }, - }, - "files": { - "delete_project": { "use": ["testuser", "metax"] } - }, - "statistics": { - "count_datasets": { "use": ["all"] }, - "all_datasets_cumulative": { "use": ["all"] }, - "catalog_datasets_cumulative": { "use": ["all"] }, - "end_user_datasets_cumulative": { "use": ["all"] }, - "harvested_datasets_cumulative": { "use": ["all"] }, - "deprecated_datasets_cumulative": { "use": ["all"] }, - "organization_datasets_cumulative": { "use": ["all"] }, - "unused_files": { "use": ["all"] }, - } - } - } -elif METAX_ENV == 'test': - # in test-env, modify API_ACCESS to give read and write perms to all (public). note that - # this affects only per-api access; nginx permissions still limits general write requests - API_ACCESS = app_config_dict['API_ACCESS'] - - for api, perms in API_ACCESS['rest'].items(): - perms['read'] = ['all'] - perms['create'] = ['all'] - perms['update'] = ['all'] - perms['delete'] = ['all'] - - for api, functions in API_ACCESS['rpc'].items(): - for function, perms in functions.items(): - perms['use'] = ['all'] -else: - # localdev, stable, production - API_ACCESS = app_config_dict['API_ACCESS'] - -if executing_in_travis: - ALLOWED_AUTH_METHODS = ['Basic', 'Bearer'] -else: - # Basic for services, Bearer for end users. Disabling Bearer auth method disables end user access - ALLOWED_AUTH_METHODS = app_config_dict['ALLOWED_AUTH_METHODS'] - -# path to local file projects -if executing_in_test_case or executing_in_travis: - ADDITIONAL_USER_PROJECTS_PATH = "/tmp/user_projects.json" -else: - ADDITIONAL_USER_PROJECTS_PATH = app_config_dict.get('ADDITIONAL_USER_PROJECTS_PATH', '') - -if executing_in_test_case or executing_in_travis: - IDA_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-ida" - ATT_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-att" - PAS_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-pas" - LEGACY_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-legacy" - EXT_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-ext" - DFT_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-dft" - -else: - IDA_DATA_CATALOG_IDENTIFIER = app_config_dict['IDA_DATACATALOG_IDENTIFIER'] - ATT_DATA_CATALOG_IDENTIFIER = app_config_dict['ATT_DATACATALOG_IDENTIFIER'] - PAS_DATA_CATALOG_IDENTIFIER = app_config_dict['PAS_DATACATALOG_IDENTIFIER'] - LEGACY_DATA_CATALOG_IDENTIFIER = app_config_dict['LEGACY_DATACATALOG_IDENTIFIER'] - DFT_DATA_CATALOG_IDENTIFIER = app_config_dict['DFT_DATACATALOG_IDENTIFIER'] - -if executing_in_test_case or executing_in_travis: - END_USER_ALLOWED_DATA_CATALOGS = [ - IDA_DATA_CATALOG_IDENTIFIER, - ATT_DATA_CATALOG_IDENTIFIER, - PAS_DATA_CATALOG_IDENTIFIER, - LEGACY_DATA_CATALOG_IDENTIFIER, - DFT_DATA_CATALOG_IDENTIFIER - ] - - LEGACY_CATALOGS = [ - LEGACY_DATA_CATALOG_IDENTIFIER, - ] -else: - # allow end users to create catalogrecords only to the following data catalogs - END_USER_ALLOWED_DATA_CATALOGS = app_config_dict['END_USER_ALLOWED_DATA_CATALOGS'] - - # catalogs where uniqueness of dataset pids is not enforced. - LEGACY_CATALOGS = [ - LEGACY_DATA_CATALOG_IDENTIFIER, - ] - -# endpoint in localhost where bearer tokens should be sent for validation -VALIDATE_TOKEN_URL = 'https://127.0.0.1/secure/validate_token' - -if executing_in_test_case or executing_in_travis: - CHECKSUM_ALGORITHMS = ['SHA-256', 'MD5', 'SHA-512'] -else: - CHECKSUM_ALGORITHMS = app_config_dict['CHECKSUM_ALGORITHMS'] - -if executing_in_test_case or executing_in_travis: - ERROR_FILES_PATH = '/tmp/metax-api-tests/errors' -else: - # location to store information about exceptions occurred during api requests - ERROR_FILES_PATH = '/var/log/metax-api/errors' - - -# Allow only specific hosts to access the app -ALLOWED_HOSTS = ['localhost', '127.0.0.1', '[::1]'] -if not os.getenv('TRAVIS', None): - SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - USE_X_FORWARDED_HOST = True - for allowed_host in app_config_dict['ALLOWED_HOSTS']: - ALLOWED_HOSTS.append(allowed_host) - -if executing_in_travis: - SERVER_DOMAIN_NAME = 'not set' - AUTH_SERVER_LOGOUT_URL = 'not set' -else: - SERVER_DOMAIN_NAME = app_config_dict['SERVER_DOMAIN_NAME'] - AUTH_SERVER_LOGOUT_URL = app_config_dict['AUTH_SERVER_LOGOUT_URL'] - - -# SECURITY WARNING: don't run with debug turned on in production! -if executing_in_travis: - DEBUG = True -else: - DEBUG = app_config_dict['DEBUG'] - -# when using the requests-library or similar, should be used to decide when to verify self-signed certs -TLS_VERIFY = False if DEBUG else True - -# Application definition - -AUTH_USER_MODEL = 'metax_api.MetaxUser' - -INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'rest_framework', - 'metax_api', -] - -if DEBUG: - INSTALLED_APPS.append('django.contrib.staticfiles') - -MIDDLEWARE = [ - 'metax_api.middleware.RequestLogging', - # note: not strictly necessary if running in a private network - # https://docs.djangoproject.com/en/1.11/ref/middleware/#module-django.middleware.security - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'metax_api.middleware.IdentifyApiCaller', - 'metax_api.middleware.AddLastModifiedHeaderToResponse', - 'metax_api.middleware.StreamHttpResponse', -] - -if not (executing_in_test_case or executing_in_travis): - # security settings - CSRF_COOKIE_SECURE = True - # SECURE_BROWSER_XSS_FILTER = True # is set in nginx - # SECURE_CONTENT_TYPE_NOSNIFF = True # is set in nginx - # SECURE_SSL_REDIRECT = True # is set in nginx - # SESSION_COOKIE_SECURE = True # is set in nginx - # X_FRAME_OPTIONS = 'DENY' # is set in nginx - -REST_FRAMEWORK = { - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' - ], - 'DEFAULT_FILTER_BACKENDS': [ - 'rest_framework.filters.OrderingFilter' - ], - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', - 'PAGE_SIZE': 10 -} - - -REST_FRAMEWORK['DEFAULT_PARSER_CLASSES'] = [ - 'rest_framework.parsers.JSONParser', - 'metax_api.parsers.XMLParser', -] - -if DEBUG: - # the renderer is selected based on the 'Accept' HTTP header - REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = [ - 'rest_framework.renderers.JSONRenderer', - 'rest_framework.renderers.BrowsableAPIRenderer', - 'metax_api.renderers.XMLRenderer', - ] -else: - # the renderer is selected based on the 'Accept' HTTP header - REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = [ - 'rest_framework.renderers.JSONRenderer', - 'metax_api.renderers.HTMLToJSONRenderer', - 'metax_api.renderers.XMLRenderer', - ] - - -ROOT_URLCONF = 'metax_api.urls' - -APPEND_SLASH = False - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'metax_api.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/1.11/ref/settings/#databases - -""" -The following uses the 'TRAVIS' (== True) environment variable on Travis -to detect the session, and changes the default database accordingly. -""" -if executing_in_travis: - DATABASES = { - 'default': { - 'NAME': 'metax_db_test', - 'USER': 'metax_test', - 'PASSWORD': '', - 'HOST': 'localhost', - } - } -else: - DATABASES = { - 'default': { - 'NAME': app_config_dict['METAX_DATABASE'], - 'USER': app_config_dict['METAX_DATABASE_USER'], - 'PASSWORD': app_config_dict['METAX_DATABASE_PASSWORD'], - 'HOST': app_config_dict['METAX_DATABASE_HOST'], - 'PORT': '', - # default is 0 == "reconnect to db on every request". placing setting here for visibility - 'CONN_MAX_AGE': 0, - } - } - -DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql' -DATABASES['default']['ATOMIC_REQUESTS'] = True - -""" -Colorize automated test console output -""" -RAINBOWTESTS_HIGHLIGHT_PATH = BASE_DIR -TEST_RUNNER = 'rainbowtests.test.runner.RainbowDiscoverRunner' - -""" -Logging rules: -- Django DEBUG enabled: Print everything from logging level DEBUG and up, to -both console, and log file. -- Django DEBUG disabled: Print everything from logging level INFO and up, only -to log file. -""" -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'standard': { - # timestamp, process id, python module name, loglevel, msg content... - 'format': '%(asctime)s p%(process)d %(name)s %(levelname)s: %(message)s', - 'datefmt': '%Y-%m-%dT%H:%M:%S.%03dZ', - }, - }, - 'filters': { - 'require_debug_true': { - '()': 'django.utils.log.RequireDebugTrue', - }, - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse', - }, - }, - 'handlers': { - 'console': { - 'level': 'DEBUG', - 'filters': ['require_debug_true'], - 'class': 'logging.StreamHandler', - 'formatter': 'standard', - }, - 'debug': { - 'level': 'DEBUG', - 'class': 'logging.FileHandler', - 'filename': '/var/log/metax-api/metax_api.log', - 'formatter': 'standard', - 'filters': ['require_debug_true'], - }, - 'general': { - 'level': 'INFO', - 'class': 'logging.FileHandler', - 'filename': '/var/log/metax-api/metax_api.log', - 'formatter': 'standard', - 'filters': ['require_debug_false'], - }, - }, - 'loggers': { - 'django': { - 'handlers': ['general', 'console', 'debug'], - }, - 'metax_api': { - 'handlers': ['general', 'console', 'debug'], - }, - } -} - -logging.Formatter.converter = time.gmtime -logger = logging.getLogger('metax_api') -logger.setLevel(logging.DEBUG if DEBUG else logging.INFO) - -structlog.configure( - processors=[ - structlog.stdlib.add_log_level, - structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.JSONRenderer() - ], - context_class=dict, - logger_factory=structlog.stdlib.LoggerFactory(), - wrapper_class=structlog.stdlib.BoundLogger, - cache_logger_on_first_use=True, -) - -handler = logging.FileHandler('/var/log/metax-api/metax_api.json.log') -handler.setFormatter(logging.Formatter('%(message)s')) -json_logger = logging.getLogger('structlog') -json_logger.addHandler(handler) -json_logger.setLevel(logging.INFO) - - -# Internationalization -# https://docs.djangoproject.com/en/1.11/topics/i18n/ - -LANGUAGE_CODE = 'en-US' - -TIME_ZONE = 'Europe/Helsinki' - -# A boolean that specifies whether Django’s translation system should -# be enabled. This provides an easy way to turn it off, for performance. -# If this is set to False, Django will make some optimizations so as not -# to load the translation machinery. -USE_I18N = True - -# A boolean that specifies if localized formatting of data will -# be enabled by default or not. If this is set to True, -# e.g. Django will display numbers and dates using the format -# of the current locale. -USE_L10N = False - -# A boolean that specifies if datetimes will be timezone-aware by default -# or not. If this is set to True, Django will use timezone-aware datetimes -# internally. Otherwise, Django will use naive datetimes in local time. -USE_TZ = True - -DATETIME_INPUT_FORMATS = ['%Y-%m-%dT%H:%M:%S.%fZ'] - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.11/howto/static-files/ - -PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) -# same dir as manage.py -STATIC_ROOT = os.path.join(os.path.dirname(PROJECT_DIR), 'static') -STATIC_URL = '/static/' - -if not executing_in_travis: - # settings for custom redis-py cache helper in utils/redis.py - REDIS = { - 'PASSWORD': app_config_dict['REDIS']['PASSWORD'], - 'LOCALHOST_PORT': app_config_dict['REDIS']['LOCALHOST_PORT'], - - # https://github.com/andymccurdy/redis-py/issues/485#issuecomment-44555664 - 'SOCKET_TIMEOUT': 0.1, - - # db index reserved for test suites - 'TEST_DB': app_config_dict['REDIS']['TEST_DB'], - - # enables extra logging to console during cache usage - 'DEBUG': False, - - 'SENTINEL': { - 'HOSTS': app_config_dict['REDIS']['SENTINEL']['HOSTS'], - 'SERVICE': app_config_dict['REDIS']['SENTINEL']['SERVICE'] - } - } - -if executing_in_travis: - ELASTICSEARCH = { - 'HOSTS': ['metax.fd-test.csc.fi/es'], - 'USE_SSL': True, - 'ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART': False, - } -else: - ELASTICSEARCH = { - 'HOSTS': app_config_dict['ELASTICSEARCH']['HOSTS'], - # normally cache is reloaded from elasticsearch only if reference data is missing. - # for one-off reload / debugging / development, use below flag - 'ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART': app_config_dict['ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART'], - } - -# given in seconds. Used only if ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART=false -ELASTICSEARCH['REFERENCE_DATA_RELOAD_INTERVAL'] = 86400 - -if not executing_in_travis: - RABBITMQ = { - 'HOSTS': app_config_dict['RABBITMQ']['HOSTS'], - 'PORT': app_config_dict['RABBITMQ']['PORT'], - 'USER': app_config_dict['RABBITMQ']['USER'], - 'VHOST': app_config_dict['RABBITMQ']['VHOST'], - 'VHOST_TTV': app_config_dict['RABBITMQ']['VHOST_TTV'], - 'PASSWORD': app_config_dict['RABBITMQ']['PASSWORD'], - 'EXCHANGES': [ - { - 'NAME': 'datasets', - 'TYPE': 'direct', - # make rabbitmq remember queues after restarts - 'DURABLE': True - }, - { - 'NAME': 'TTV-datasets', - 'TYPE': 'fanout', - # make rabbitmq remember queues after restarts - 'DURABLE': True - } - ] - } - -if executing_in_travis: - OAI = { - 'BASE_URL': 'http://metax.fd-test.csc.fi/oai/', - 'BATCH_SIZE': 25, - 'REPOSITORY_NAME': 'Metax', - 'ETSIN_URL_TEMPLATE': 'http://etsin.something.fi/dataset/%s', - 'ADMIN_EMAIL': 'noreply@csc.fi', - 'SET_MAPPINGS': { - 'datasets': [ - IDA_DATA_CATALOG_IDENTIFIER, - ATT_DATA_CATALOG_IDENTIFIER - ], - 'ida_datasets': [ - IDA_DATA_CATALOG_IDENTIFIER - ], - 'att_datasets': [ - ATT_DATA_CATALOG_IDENTIFIER - ] - } - } -else: - OAI = { - 'BASE_URL': app_config_dict['OAI']['BASE_URL'], - 'BATCH_SIZE': app_config_dict['OAI']['BATCH_SIZE'], - 'REPOSITORY_NAME': app_config_dict['OAI']['REPOSITORY_NAME'], - 'ETSIN_URL_TEMPLATE': app_config_dict['OAI']['ETSIN_URL_TEMPLATE'], - 'ADMIN_EMAIL': app_config_dict['OAI']['ADMIN_EMAIL'], - 'SET_MAPPINGS': app_config_dict['OAI']['SET_MAPPINGS'] - } - -if executing_in_travis: - DATACITE = { - 'USERNAME': 'datacite_user', - 'PASSWORD': 'datacite_password', - 'ETSIN_URL_TEMPLATE': 'https://etsin.something.fi/dataset/%s', - 'PREFIX': '10.5072' - } -else: - DATACITE = { - 'USERNAME': app_config_dict['DATACITE']['USERNAME'], - 'PASSWORD': app_config_dict['DATACITE']['PASSWORD'], - 'ETSIN_URL_TEMPLATE': app_config_dict['DATACITE']['ETSIN_URL_TEMPLATE'], - 'PREFIX': app_config_dict['DATACITE']['PREFIX'], - 'URL': app_config_dict['DATACITE']['URL'], - } - -if executing_in_travis: - REMS = { - 'ENABLED': True, - 'API_KEY': 'key', - 'BASE_URL': 'https://rems.instance.fi', - 'ETSIN_URL_TEMPLATE': 'https://etsin.something.fi/dataset/%s', - 'METAX_USER': 'muser', - 'REPORTER_USER': 'ruser', - 'AUTO_APPROVER': 'aappr', - 'FORM_ID': 0, - } -else: - REMS = { - 'ENABLED': app_config_dict.get('REMS', {}).get('ENABLED'), - 'API_KEY': str(app_config_dict.get('REMS', {}).get('API_KEY')), - 'BASE_URL': app_config_dict.get('REMS', {}).get('BASE_URL'), - 'ETSIN_URL_TEMPLATE': app_config_dict.get('REMS', {}).get('ETSIN_URL_TEMPLATE'), - 'METAX_USER': app_config_dict.get('REMS', {}).get('METAX_USER'), - 'REPORTER_USER': app_config_dict.get('REMS', {}).get('REPORTER_USER'), - 'AUTO_APPROVER': app_config_dict.get('REMS', {}).get('AUTO_APPROVER'), - 'FORM_ID': int(app_config_dict.get('REMS', {}).get('FORM_ID')), - } - -# draft_enabled flag controls v1 drafts. v2 api has always drafts enabled -if executing_in_test_case or executing_in_travis: - DRAFT_ENABLED = True -else: - DRAFT_ENABLED = app_config_dict.get('DRAFT_ENABLED', False) - -if executing_in_test_case or executing_in_travis: - API_VERSIONS_ENABLED = [ - "v1", - "v2" - ] -else: - API_VERSIONS_ENABLED = app_config_dict.get('API_VERSIONS_ENABLED', []) - -if executing_in_test_case or executing_in_travis: - RABBITMQ_FOR_TTV_ENABLED = True -else: - RABBITMQ_FOR_TTV_ENABLED = app_config_dict.get('RABBITMQ_FOR_TTV_ENABLED', False) diff --git a/src/metax_api/settings/.env.template b/src/metax_api/settings/.env.template new file mode 100755 index 00000000..9abf430d --- /dev/null +++ b/src/metax_api/settings/.env.template @@ -0,0 +1,16 @@ +AUTH_SERVER_LOGOUT_URL= +DATACITE_PREFIX= +DATACITE_URL= +DATACITE_PASSWORD= +DATACITE_ETSIN_URL_TEMPLATE= +DATACITE_USERNAME= +DEBUG=on +DJANGO_SECRET_KEY= +DRAFT_ENABLED=true +METAX_DATABASE= +METAX_DATABASE_PASSWORD= +METAX_DATABASE_USER= +OAI_BASE_URL= +OAI_ETSIN_URL_TEMPLATE= +RABBIT_MQ_VHOST= +REMS_ENABLED=false diff --git a/src/metax_api/settings/__init__.py b/src/metax_api/settings/__init__.py new file mode 100755 index 00000000..42dccade --- /dev/null +++ b/src/metax_api/settings/__init__.py @@ -0,0 +1,78 @@ +""" +This is a django-split-settings main file. +For more information read this: +https://github.com/sobolevn/django-split-settings +To change settings file: +`DJANGO_ENV=production python manage.py runserver` +""" +from os.path import join + +import environ +from icecream import ic +from split_settings.tools import include + +from metax_api.settings.components import BASE_DIR # src + +# Managing environment via DJANGO_ENV variable: +REFDATA_INDEXER_PATH = join( + BASE_DIR, "metax_api", "tasks", "refdata", "refdata_indexer" +) +env = environ.Env( + # set casting, default value + ADDITIONAL_USER_PROJECTS_PATH=(str, ""), + ALLOWED_HOSTS=(list, ["metax.csc.local", "20.20.20.20"]), + DEBUG=(bool, False), + DJANGO_ENV=(str, "local"), + ELASTIC_SEARCH_PORT=(int, 9200), + ELASTIC_SEARCH_USE_SSL=(bool, False), + ENABLE_V1_ENDPOINTS=(bool, True), + ENABLE_V2_ENDPOINTS=(bool, True), + ERROR_FILES_PATH=(str, join(BASE_DIR, "log", "errors")), + ES_CONFIG_DIR=(str, join(REFDATA_INDEXER_PATH, "resources", "es-config/")), + LOCAL_REF_DATA_FOLDER=(str,join(REFDATA_INDEXER_PATH, "resources", "local-refdata/"),), + LOGGING_DEBUG_HANDLER_FILE=(str, join(BASE_DIR, "log", "metax_api.log")), + LOGGING_GENERAL_HANDLER_FILE=(str, join(BASE_DIR, "log", "metax_api.log")), + LOGGING_JSON_FILE_HANDLER_FILE=(str, join(BASE_DIR, "log", "metax_api.json.log")), + METAX_DATABASE_HOST=(str, "localhost"), + METAX_DATABASE_PORT=(str, 5432), + METAX_ENV=(str, "local_development"), + METAX_API_ROOT=(str, "https://localhost:8008"), + ORG_FILE_PATH=(str, join(REFDATA_INDEXER_PATH, "resources", "organizations", "organizations.csv"),), + OAI_REPOSITORY_NAME=(str, "Metax"), + RABBIT_MQ_HOSTS=(list, ["localhost"]), + RABBIT_MQ_PORT=(int, 5672), + RABBIT_MQ_PASSWORD=(str, "guest"), + RABBIT_MQ_USER=(str, "guest"), + RABBIT_MQ_USE_VHOST=(bool, False), + REDIS_HOST=(str, "localhost"), + REDIS_PORT=(int, 6379), + REDIS_TEST_DB=(int, 15), + REDIS_USE_PASSWORD=(bool, False), + REMS_ENABLED=(bool, False), + SERVER_DOMAIN_NAME=(str, "metax.csc.local"), + TRAVIS=(bool, False), + VALIDATE_TOKEN_URL=(str, "https://127.0.0.1/secure/validate_token"), + WKT_FILENAME=(str, join(REFDATA_INDEXER_PATH, "resources", "uri_to_wkt.json")), +) +# reading .env file +environ.Env.read_env() + +ENV = env("DJANGO_ENV") + +base_settings = [ + "components/common.py", + "components/logging.py", + "components/redis.py", + "components/access_control.py", + "components/elasticsearch.py", + "components/rabbitmq.py", + "components/externals.py", + "components/rems.py", + "environments/{0}.py".format(ENV), + # Optionally override some settings: + # optional('environments/legacy.py'), +] +ic(ENV) + +# Include settings: +include(*base_settings) diff --git a/src/metax_api/settings/components/__init__.py b/src/metax_api/settings/components/__init__.py new file mode 100755 index 00000000..6e3dfd45 --- /dev/null +++ b/src/metax_api/settings/components/__init__.py @@ -0,0 +1,3 @@ +from pathlib import PurePath + +BASE_DIR = PurePath(__file__).parent.parent.parent.parent diff --git a/src/metax_api/settings/components/access_control.py b/src/metax_api/settings/components/access_control.py new file mode 100755 index 00000000..1096b8b4 --- /dev/null +++ b/src/metax_api/settings/components/access_control.py @@ -0,0 +1,71 @@ +# specify read and write access of services per api, or if an api is readable by world +API_ACCESS = { + "rest": { + "apierrors": {"create": ["metax"], "delete": ["metax"], "read": ["metax"]}, + "contracts": { + "create": ["metax", "tpas"], + "delete": ["metax", "tpas"], + "read": ["metax", "tpas"], + "update": ["metax", "tpas"], + }, + "datacatalogs": { + "create": ["metax", "etsin"], + "delete": ["metax", "etsin"], + "read": ["metax", "all"], + "update": ["metax", "etsin"], + }, + "datasets": { + "create": ["metax", "qvain", "etsin", "tpas", "endusers"], + "delete": ["metax", "qvain", "etsin", "tpas", "endusers"], + "read": ["all"], + "update": ["metax", "qvain", "etsin", "tpas", "endusers"], + }, + "directories": { + "create": [], + "delete": [], + "read": ["metax", "qvain", "etsin", "tpas", "fds", "endusers"], + "update": [], + }, + "files": { + "create": ["metax", "ida", "tpas"], + "delete": ["metax", "ida", "tpas"], + "read": ["metax", "ida", "fds", "tpas", "endusers"], + "update": ["metax", "ida", "tpas", "endusers"], + }, + "filestorages": { + "create": ["metax"], + "delete": ["metax"], + "read": ["metax"], + "update": ["metax"], + }, + "schemas": {"create": [], "delete": [], "read": ["all"], "update": []}, + }, + "rpc": { + "datasets": { + "change_cumulative_state": {"use": ["all"]}, + "create_draft": {"use": ["all"]}, + "create_new_version": {"use": ["all"]}, + "fix_deprecated": {"use": ["all"]}, + "get_minimal_dataset_template": {"use": ["all"]}, + "merge_draft": {"use": ["all"]}, + "publish_dataset": {"use": ["all"]}, + "refresh_directory_content": {"use": ["all"]}, + "set_preservation_identifier": {"use": ["metax", "tpas"]}, + }, + "elasticsearchs": {"map_refdata": {"use": ["all"]}}, + "files": { + "delete_project": {"use": ["metax", "ida", "tpas"]}, + "flush_project": {"use": ["metax", "ida", "tpas"]}, + }, + "statistics": { + "all_datasets_cumulative": {"use": ["all"]}, + "catalog_datasets_cumulative": {"use": ["all"]}, + "count_datasets": {"use": ["all"]}, + "deprecated_datasets_cumulative": {"use": ["all"]}, + "end_user_datasets_cumulative": {"use": ["all"]}, + "harvested_datasets_cumulative": {"use": ["all"]}, + "organization_datasets_cumulative": {"use": ["all"]}, + "unused_files": {"use": ["all"]}, + }, + }, +} diff --git a/src/metax_api/settings/components/common.py b/src/metax_api/settings/components/common.py new file mode 100755 index 00000000..aa060a31 --- /dev/null +++ b/src/metax_api/settings/components/common.py @@ -0,0 +1,196 @@ +import os + +from metax_api.settings import env +from metax_api.settings.components import BASE_DIR + +DEBUG = env("DEBUG") +SECRET_KEY = env("DJANGO_SECRET_KEY") +METAX_API_ROOT = env("METAX_API_ROOT") +ADDITIONAL_USER_PROJECTS_PATH = env("ADDITIONAL_USER_PROJECTS_PATH") +IDA_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-ida" +ATT_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-att" +PAS_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-pas" +LEGACY_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-legacy" +EXT_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-ext" +DFT_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-dft" + +END_USER_ALLOWED_DATA_CATALOGS = [ + IDA_DATA_CATALOG_IDENTIFIER, + ATT_DATA_CATALOG_IDENTIFIER, + LEGACY_DATA_CATALOG_IDENTIFIER, + DFT_DATA_CATALOG_IDENTIFIER, +] + +# catalogs where uniqueness of dataset pids is not enforced. +LEGACY_CATALOGS = [ + LEGACY_DATA_CATALOG_IDENTIFIER, +] +VALIDATE_TOKEN_URL = env("VALIDATE_TOKEN_URL") +CHECKSUM_ALGORITHMS = ["SHA-256", "MD5", "SHA-512"] +ERROR_FILES_PATH = env("ERROR_FILES_PATH") + +# Allow only specific hosts to access the app +ALLOWED_HOSTS = ["localhost", "127.0.0.1", "[::1]"] + +# SITE_URL = "localhost:8008" + +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +USE_X_FORWARDED_HOST = True +for allowed_host in env("ALLOWED_HOSTS"): + ALLOWED_HOSTS.append(allowed_host) + +SERVER_DOMAIN_NAME = env("SERVER_DOMAIN_NAME") +AUTH_SERVER_LOGOUT_URL = env("AUTH_SERVER_LOGOUT_URL") + +# when using the requests-library or similar, should be used to decide when to verify self-signed certs +TLS_VERIFY = False if DEBUG else True + +AUTH_USER_MODEL = "metax_api.MetaxUser" +ALLOWED_AUTH_METHODS = ["Basic", "Bearer"] + +INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "rest_framework", + "metax_api", + "sslserver", +] + +if DEBUG: + INSTALLED_APPS.append("django.contrib.staticfiles") + +MIDDLEWARE = [ + "metax_api.middleware.RequestLogging", + # note: not strictly necessary if running in a private network + # https://docs.djangoproject.com/en/1.11/ref/middleware/#module-django.middleware.security + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "metax_api.middleware.IdentifyApiCaller", + "metax_api.middleware.AddLastModifiedHeaderToResponse", + "metax_api.middleware.StreamHttpResponse", +] + +# security settings +CSRF_COOKIE_SECURE = True +# SECURE_BROWSER_XSS_FILTER = True # is set in nginx +# SECURE_CONTENT_TYPE_NOSNIFF = True # is set in nginx +# SECURE_SSL_REDIRECT = True # is set in nginx +# SESSION_COOKIE_SECURE = True # is set in nginx +# X_FRAME_OPTIONS = 'DENY' # is set in nginx + +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" + ], + "DEFAULT_FILTER_BACKENDS": ["rest_framework.filters.OrderingFilter"], + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", + "PAGE_SIZE": 10, +} +REST_FRAMEWORK["DEFAULT_PARSER_CLASSES"] = [ + "rest_framework.parsers.JSONParser", + "metax_api.parsers.XMLParser", +] +REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] = [ + "rest_framework.renderers.JSONRenderer", + "metax_api.renderers.HTMLToJSONRenderer", + "metax_api.renderers.XMLRenderer", +] +ROOT_URLCONF = "metax_api.urls" + +APPEND_SLASH = False + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] +WSGI_APPLICATION = "metax_api.wsgi.application" + +DATABASES = { + "default": { + "NAME": env("METAX_DATABASE"), + "USER": env("METAX_DATABASE_USER"), + "PASSWORD": env("METAX_DATABASE_PASSWORD"), + "HOST": env("METAX_DATABASE_HOST"), + "PORT": env("METAX_DATABASE_PORT"), + # default is 0 == "reconnect to db on every request". placing setting here for visibility + "CONN_MAX_AGE": 0, + } +} + +DATABASES["default"]["ENGINE"] = "django.db.backends.postgresql" +DATABASES["default"]["ATOMIC_REQUESTS"] = True + + +# Colorize automated test console output +RAINBOWTESTS_HIGHLIGHT_PATH = str(BASE_DIR) +TEST_RUNNER = "rainbowtests.test.runner.RainbowDiscoverRunner" + +# Internationalization +# https://docs.djangoproject.com/en/1.11/topics/i18n/ + +LANGUAGE_CODE = "en-US" + +TIME_ZONE = "Europe/Helsinki" + +# A boolean that specifies whether Django’s translation system should +# be enabled. This provides an easy way to turn it off, for performance. +# If this is set to False, Django will make some optimizations so as not +# to load the translation machinery. +USE_I18N = True + +# A boolean that specifies if localized formatting of data will +# be enabled by default or not. If this is set to True, +# e.g. Django will display numbers and dates using the format +# of the current locale. +USE_L10N = False + +# A boolean that specifies if datetimes will be timezone-aware by default +# or not. If this is set to True, Django will use timezone-aware datetimes +# internally. Otherwise, Django will use naive datetimes in local time. +USE_TZ = True + +DATETIME_INPUT_FORMATS = ["%Y-%m-%dT%H:%M:%S.%fZ"] + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.11/howto/static-files/ + +PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) +# same dir as manage.py +STATIC_ROOT = os.path.join(os.path.dirname(PROJECT_DIR), "static") +STATIC_URL = "/static/" + +API_VERSIONS_ENABLED = [] +if env("ENABLE_V1_ENDPOINTS"): + API_VERSIONS_ENABLED.append("v1") +if env("ENABLE_V2_ENDPOINTS"): + API_VERSIONS_ENABLED.append("v2") +DRAFT_ENABLED = env("DRAFT_ENABLED") + +# Variables related to api credentials +API_USERS = [ + {"password": "test-metax", "username": "metax"}, + {"password": "test-qvain", "username": "qvain"}, + {"password": "test-ida", "username": "ida"}, + {"password": "test-tpas", "username": "tpas"}, + {"password": "test-etsin", "username": "etsin"}, + {"password": "test-fds", "username": "fds"}, +] diff --git a/src/metax_api/settings/components/elasticsearch.py b/src/metax_api/settings/components/elasticsearch.py new file mode 100755 index 00000000..de9e9ae1 --- /dev/null +++ b/src/metax_api/settings/components/elasticsearch.py @@ -0,0 +1,13 @@ +from metax_api.settings.components.common import env + +# INSTALLED_APPS += ["django_elasticsearch_dsl"] +ES_HOSTS = [env("ELASTIC_SEARCH_HOSTS")] + +ELASTICSEARCH = { + "HOSTS": ES_HOSTS, + "USE_SSL": env("ELASTIC_SEARCH_USE_SSL"), + "ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART": False, +} +ELASTICSEARCH["REFERENCE_DATA_RELOAD_INTERVAL"] = 86400 + +ES_CONFIG_DIR = env("ES_CONFIG_DIR") diff --git a/src/metax_api/settings/components/externals.py b/src/metax_api/settings/components/externals.py new file mode 100755 index 00000000..c387625f --- /dev/null +++ b/src/metax_api/settings/components/externals.py @@ -0,0 +1,28 @@ +from metax_api.settings import env +from metax_api.settings.components.common import ( + IDA_DATA_CATALOG_IDENTIFIER, + ATT_DATA_CATALOG_IDENTIFIER, +) + +OAI = { + "BASE_URL": env("OAI_BASE_URL"), + "BATCH_SIZE": 25, + "REPOSITORY_NAME": env("OAI_REPOSITORY_NAME"), + "ETSIN_URL_TEMPLATE": env("OAI_ETSIN_URL_TEMPLATE"), + "ADMIN_EMAIL": env("OAI_ADMIN_EMAIL"), + "SET_MAPPINGS": { + "datasets": [IDA_DATA_CATALOG_IDENTIFIER, ATT_DATA_CATALOG_IDENTIFIER], + "ida_datasets": [IDA_DATA_CATALOG_IDENTIFIER], + "att_datasets": [ATT_DATA_CATALOG_IDENTIFIER], + }, +} +DATACITE = { + "USERNAME": env("DATACITE_USERNAME"), + "PASSWORD": env("DATACITE_PASSWORD"), + "ETSIN_URL_TEMPLATE": env("DATACITE_ETSIN_URL_TEMPLATE"), + "PREFIX": env("DATACITE_PREFIX"), + "URL": env("DATACITE_URL"), +} +ORG_FILE_PATH = env("ORG_FILE_PATH") +WKT_FILENAME = env("WKT_FILENAME") +LOCAL_REF_DATA_FOLDER = env("LOCAL_REF_DATA_FOLDER") diff --git a/src/metax_api/settings/components/logging.py b/src/metax_api/settings/components/logging.py new file mode 100755 index 00000000..7e17e2ea --- /dev/null +++ b/src/metax_api/settings/components/logging.py @@ -0,0 +1,88 @@ +import logging.config +import time + +import structlog + +from metax_api.settings import env +from metax_api.settings.components.common import DEBUG + +# Logging rules: +# - Django DEBUG enabled: Print everything from logging level DEBUG and up, to both console, and log file. +# - Django DEBUG disabled: Print everything from logging level INFO and up, only to log file. + +LOGGING_DEBUG_HANDLER_FILE = env("LOGGING_DEBUG_HANDLER_FILE") +LOGGING_JSON_FILE_HANDLER_FILE = env("LOGGING_JSON_FILE_HANDLER_FILE") +LOGGING_GENERAL_HANDLER_FILE = env("LOGGING_GENERAL_HANDLER_FILE") + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "standard": { + # timestamp, process id, python module name, loglevel, msg content... + "format": "%(asctime)s p%(process)d %(name)s %(levelname)s: %(message)s", + "datefmt": "%Y-%m-%dT%H:%M:%S.%03dZ", + }, + }, + "filters": { + "require_debug_true": { + "()": "django.utils.log.RequireDebugTrue", + }, + "require_debug_false": { + "()": "django.utils.log.RequireDebugFalse", + }, + }, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "standard", + "filters": ["require_debug_true"], + }, + "debug": { + "level": "DEBUG", + "class": "logging.FileHandler", + "filename": LOGGING_DEBUG_HANDLER_FILE, + "formatter": "standard", + "filters": ["require_debug_true"], + }, + "general": { + "level": "INFO", + "class": "logging.FileHandler", + "filename": LOGGING_DEBUG_HANDLER_FILE, + "formatter": "standard", + "filters": ["require_debug_false"], + }, + }, + "loggers": { + "django": { + "handlers": ["general", "console", "debug"], + }, + "metax_api": { + "handlers": ["general", "console", "debug"], + }, + "root": {"level": "INFO", "handlers": ["console"]}, + }, +} + +logging.Formatter.converter = time.gmtime +logger = logging.getLogger("metax_api") +logger.setLevel(logging.DEBUG if DEBUG else logging.INFO) + +structlog.configure( + processors=[ + structlog.stdlib.add_log_level, + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.JSONRenderer(), + ], + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, +) + +handler = logging.FileHandler(LOGGING_JSON_FILE_HANDLER_FILE) +handler.setFormatter(logging.Formatter("%(message)s")) +json_logger = logging.getLogger("structlog") +json_logger.addHandler(handler) +json_logger.setLevel(logging.INFO) diff --git a/src/metax_api/settings/components/rabbitmq.py b/src/metax_api/settings/components/rabbitmq.py new file mode 100755 index 00000000..8bf1809b --- /dev/null +++ b/src/metax_api/settings/components/rabbitmq.py @@ -0,0 +1,31 @@ +from metax_api.settings import env + +RABBITMQ = { + "HOSTS": env("RABBIT_MQ_HOSTS"), + "PORT": env("RABBIT_MQ_PORT"), + "USER": env("RABBIT_MQ_USER"), + "PASSWORD": env("RABBIT_MQ_PASSWORD"), + "EXCHANGES": [ + { + "NAME": "datasets", + "TYPE": "direct", + # make rabbitmq remember queues after restarts + "DURABLE": True, + }, + { + "NAME": "TTV-datasets", + "TYPE": "fanout", + "DURABLE": True, + "QUEUES": [ + { + "NAME": "ttv-operations", + #"ROUTING_KEY": "some_key" + } + ] + } + ], +} +RABBIT_MQ_USE_VHOST = env("RABBIT_MQ_USE_VHOST") + +if RABBIT_MQ_USE_VHOST: + RABBITMQ["VHOST"] = env("RABBIT_MQ_VHOST") diff --git a/src/metax_api/settings/components/redis.py b/src/metax_api/settings/components/redis.py new file mode 100755 index 00000000..b26020ef --- /dev/null +++ b/src/metax_api/settings/components/redis.py @@ -0,0 +1,34 @@ +from environ import ImproperlyConfigured +import logging +from metax_api.settings import env + +logger = logging.getLogger(__name__) + +REDIS = { + "LOCALHOST_PORT": env("REDIS_PORT"), + "HOST": env("REDIS_HOST"), + "PORT": env("REDIS_PORT"), + # https://github.com/andymccurdy/redis-py/issues/485#issuecomment-44555664 + "SOCKET_TIMEOUT": 0.1, + # db index reserved for test suites + "TEST_DB": env("REDIS_TEST_DB"), + # enables extra logging to console during cache usage + "DEBUG": False, +} +REDIS_USE_PASSWORD = env("REDIS_USE_PASSWORD") +REDIS_USE_SENTINEL = False + +if REDIS_USE_PASSWORD: + try: + REDIS["PASSWORD"] = env("REDIS_PASSWORD") + except ImproperlyConfigured as e: + logger.warning(e) + +if REDIS_USE_SENTINEL: + try: + REDIS["SENTINEL"] = { + "HOSTS": [["127.0.0.1", 16379], ["127.0.0.1", 16380], ["127.0.0.1", 16381]], + "SERVICE": env("REDIS_SENTINEL_SERVICE"), + } + except ImproperlyConfigured as e: + logger.warning(e) diff --git a/src/metax_api/settings/components/rems.py b/src/metax_api/settings/components/rems.py new file mode 100644 index 00000000..48b672d9 --- /dev/null +++ b/src/metax_api/settings/components/rems.py @@ -0,0 +1,20 @@ +from environ import ImproperlyConfigured +import logging + +logger = logging.getLogger(__name__) + +from metax_api.settings import env +REMS = { + "ENABLED": env("REMS_ENABLED"), +} +if REMS["ENABLED"]: + try: + REMS["API_KEY"] = env("REMS_API_KEY") + REMS["BASE_URL"] = env("REMS_BASE_URL") + REMS["ETSIN_URL_TEMPLATE"] = env("REMS_ETSIN_URL_TEMPLATE") + REMS["METAX_USER"] = env("REMS_METAX_USER") + REMS["REPORTER_USER"] = env("REMS_REPORTER_USER") + REMS["AUTO_APPROVER"] = env("REMS_AUTO_APPROVER") + REMS["FORM_ID"] = int(env("REMS_FORM_ID")) + except ImproperlyConfigured as e: + logger.warning(e) diff --git a/resources/data/test_data.json b/src/metax_api/settings/environments/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from resources/data/test_data.json rename to src/metax_api/settings/environments/__init__.py diff --git a/src/metax_api/settings/environments/local.py b/src/metax_api/settings/environments/local.py new file mode 100755 index 00000000..33d1d00f --- /dev/null +++ b/src/metax_api/settings/environments/local.py @@ -0,0 +1,49 @@ +from watchman import constants as watchman_constants + +from metax_api.settings.components.common import INSTALLED_APPS, ALLOWED_HOSTS + +INSTALLED_APPS += ["watchman"] + +ALLOWED_HOSTS += ["*"] + +WATCHMAN_CHECKS = watchman_constants.DEFAULT_CHECKS + ( + "metax_api.checks.elasticsearch_check", + "metax_api.checks.redis_check", +) + +# only used in manual testing script in tests/rabbitmq/consume.py +CONSUMERS = [ + { + "is_test_user": True, + "name": "testaaja", + "password": "testaaja", + "permissions": { + "conf": "^testaaja-.*$", + "read": "^(datasets|testaaja-.*)$", + "write": "^testaaja-.*$", + }, + "vhost": "metax", + }, + { + "is_test_user": False, + "name": "etsin", + "password": "test-etsin", + "permissions": { + "conf": "^etsin-.*$", + "read": "^(datasets|etsin-.*)$", + "write": "^etsin-.*$", + }, + "vhost": "metax", + }, + { + "is_test_user": False, + "name": "ttv", + "password": "test-ttv", + "permissions": { + "conf": "^ttv-.*$", + "read": "^(TTV-datasets|ttv-.*)$", + "write": "^ttv-.*$", + }, + "vhost": "metax", + }, +] diff --git a/src/metax_api/settings/environments/remote.py b/src/metax_api/settings/environments/remote.py new file mode 100644 index 00000000..c1eced5c --- /dev/null +++ b/src/metax_api/settings/environments/remote.py @@ -0,0 +1,297 @@ +from metax_api.settings import env + +API_USERS = [ + { + "password": env("METAX_USER_PASSWORD"), + "username": "metax" + }, + { + "password": env("QVAIN_USER_PASSWORD"), + "username": "qvain" + }, + { + "password": env("IDA_USER_PASSWORD"), + "username": "ida" + }, + { + "password": env("TPAS_USER_PASSWORD"), + "username": "tpas" + }, + { + "password": env("ETSIN_USER_PASSWORD"), + "username": "etsin" + }, + { + "password": env("FDS_USER_PASSWORD"), + "username": "fds" + }, + { + "password": env("QVAIN_LIGHT_USER_PASSWORD"), + "username": "qvain-light" + }, + { + "password": env("QVAIN_JORI_USER_PASSWORD"), + "username": "qvain-jori" + }, + { + "password": env("TTV_USER_PASSWORD"), + "username": "ttv" + }, + { + "password": env("DOWNLOAD_USER_PASSWORD"), + "username": "download" + } +] + +CONSUMERS = [ + { + "is_test_user": True, + "name": "testaaja", + "password": env("TESTAAJA_CONSUMER_PASSWORD"), + "permissions": { + "conf": "^testaaja-.*$", + "read": "^(datasets|testaaja-.*)$", + "write": "^testaaja-.*$" + }, + "vhost": "metax" + }, + { + "is_test_user": False, + "name": "etsin", + "password": env("ETSIN_CONSUMER_PASSWORD"), + "permissions": { + "conf": "^etsin-.*$", + "read": "^(datasets|etsin-.*)$", + "write": "^etsin-.*$" + }, + "vhost": "metax" + }, + { + "is_test_user": False, + "name": "ttv", + "password": env("TTV_CONSUMER_PASSWORD"), + "permissions": { + "conf": "^ttv-.*$", + "read": "^(TTV-datasets|ttv-.*)$", + "write": "^ttv-.*$" + }, + "vhost": "ttv" + } +] + +END_USER_ALLOWED_DATA_CATALOGS = [ + "urn:nbn:fi:att:data-catalog-ida", + "urn:nbn:fi:att:data-catalog-att", + "urn:nbn:fi:att:data-catalog-legacy", + "urn:nbn:fi:att:data-catalog-pas", + "urn:nbn:fi:att:data-catalog-dft" +] + +API_ACCESS = { + "rest": { + "apierrors": { + "delete": [ + "metax" + ], + "read": [ + "all" + ] + }, + "contracts": { + "create": [ + "all" + ], + "delete": [ + "all" + ], + "read": [ + "all" + ], + "update": [ + "all" + ] + }, + "datacatalogs": { + "create": [ + "all" + ], + "delete": [ + "all" + ], + "read": [ + "all" + ], + "update": [ + "all" + ] + }, + "datasets": { + "create": [ + "all" + ], + "delete": [ + "all" + ], + "read": [ + "all" + ], + "update": [ + "all" + ] + }, + "directories": { + "create": [], + "delete": [], + "read": [ + "all" + ], + "update": [] + }, + "files": { + "create": [ + "all" + ], + "delete": [ + "all" + ], + "read": [ + "all" + ], + "update": [ + "all" + ] + }, + "filestorages": { + "create": [ + "all" + ], + "delete": [ + "all" + ], + "read": [ + "all" + ], + "update": [ + "all" + ] + }, + "schemas": { + "create": [], + "delete": [], + "read": [ + "all" + ], + "update": [] + } + }, + "rpc": { + "datasets": { + "change_cumulative_state": { + "use": [ + "all" + ] + }, + "create_draft": { + "use": [ + "all" + ] + }, + "create_new_version": { + "use": [ + "all" + ] + }, + "fix_deprecated": { + "use": [ + "all" + ] + }, + "get_minimal_dataset_template": { + "use": [ + "all" + ] + }, + "merge_draft": { + "use": [ + "all" + ] + }, + "publish_dataset": { + "use": [ + "all" + ] + }, + "refresh_directory_content": { + "use": [ + "all" + ] + }, + "set_preservation_identifier": { + "use": [ + "all" + ] + } + }, + "elasticsearchs": { + "map_refdata": { + "use": [ + "all" + ] + } + }, + "files": { + "delete_project": { + "use": [ + "all" + ] + }, + "flush_project": { + "use": [ + "all" + ] + } + }, + "statistics": { + "all_datasets_cumulative": { + "use": [ + "all" + ] + }, + "catalog_datasets_cumulative": { + "use": [ + "all" + ] + }, + "count_datasets": { + "use": [ + "all" + ] + }, + "deprecated_datasets_cumulative": { + "use": [ + "all" + ] + }, + "end_user_datasets_cumulative": { + "use": [ + "all" + ] + }, + "harvested_datasets_cumulative": { + "use": [ + "all" + ] + }, + "organization_datasets_cumulative": { + "use": [ + "all" + ] + }, + "unused_files": { + "use": [ + "all" + ] + } + } + } +} \ No newline at end of file diff --git a/src/metax_api/settings/environments/test.py b/src/metax_api/settings/environments/test.py new file mode 100755 index 00000000..b95d9570 --- /dev/null +++ b/src/metax_api/settings/environments/test.py @@ -0,0 +1,80 @@ +API_TEST_USER = {"username": "testuser", "password": "testuserpassword"} +API_METAX_USER = {"username": "metax", "password": "metaxpassword"} +API_AUTH_TEST_USER = {"username": "api_auth_user", "password": "password"} + +API_EXT_USER = {"username": "external", "password": "externalpassword"} + +API_TEST_USERS = [API_TEST_USER, API_METAX_USER, API_AUTH_TEST_USER, API_EXT_USER] + +API_ACCESS = { + "rest": { + "apierrors": { + "read": ["testuser", "metax"], + "create": ["testuser", "metax"], + "update": ["testuser", "metax"], + "delete": ["testuser", "metax"], + }, + "contracts": { + "read": ["testuser", "metax"], + "create": ["testuser", "metax"], + "update": ["testuser", "metax"], + "delete": ["testuser", "metax"], + }, + "datacatalogs": { + "read": ["all"], + "create": ["testuser", "metax"], + "update": ["testuser", "metax"], + "delete": ["testuser", "metax"], + }, + "datasets": { + "read": ["all"], + "create": ["testuser", "metax", "api_auth_user", "endusers", "external"], + "update": ["testuser", "metax", "api_auth_user", "endusers", "external"], + "delete": ["testuser", "metax", "api_auth_user", "endusers", "external"], + }, + "directories": { + "read": ["testuser", "metax", "endusers"], + }, + "files": { + "read": ["testuser", "metax", "api_auth_user", "endusers"], + "create": ["testuser", "metax"], + "update": ["testuser", "metax", "endusers"], + "delete": ["testuser", "metax"], + }, + "filestorages": { + "read": ["testuser", "metax"], + "create": ["testuser", "metax"], + "update": ["testuser", "metax"], + "delete": ["testuser", "metax"], + }, + "schemas": { + "read": ["all"], + }, + }, + "rpc": { + "datasets": { + "change_cumulative_state": {"use": ["all"]}, + "get_minimal_dataset_template": {"use": ["all"]}, + "refresh_directory_content": {"use": ["all"]}, + "fix_deprecated": {"use": ["all"]}, + "set_preservation_identifier": {"use": ["metax", "tpas"]}, + "create_new_version": {"use": ["all"]}, + "publish_dataset": {"use": ["all"]}, + "create_draft": {"use": ["all"]}, + "merge_draft": {"use": ["all"]}, + }, + "files": {"delete_project": {"use": ["testuser", "metax"]}}, + "statistics": { + "count_datasets": {"use": ["all"]}, + "all_datasets_cumulative": {"use": ["all"]}, + "catalog_datasets_cumulative": {"use": ["all"]}, + "end_user_datasets_cumulative": {"use": ["all"]}, + "harvested_datasets_cumulative": {"use": ["all"]}, + "deprecated_datasets_cumulative": {"use": ["all"]}, + "organization_datasets_cumulative": {"use": ["all"]}, + "unused_files": {"use": ["all"]}, + }, + }, +} + +ADDITIONAL_USER_PROJECTS_PATH = "/tmp/user_projects.json" diff --git a/src/metax_api/settings/environments/travis.py b/src/metax_api/settings/environments/travis.py new file mode 100755 index 00000000..5bfca59f --- /dev/null +++ b/src/metax_api/settings/environments/travis.py @@ -0,0 +1,9 @@ +from metax_api.settings.environments import test + +API_TEST_USER = test.API_TEST_USER +API_METAX_USER = test.API_METAX_USER +API_AUTH_TEST_USER = test.API_AUTH_TEST_USER +API_EXT_USER = test.API_EXT_USER +API_TEST_USERS = test.API_TEST_USERS +API_ACCESS = test.API_ACCESS +ADDITIONAL_USER_PROJECTS_PATH = test.ADDITIONAL_USER_PROJECTS_PATH diff --git a/src/metax_api/tasks/__init__.py b/src/metax_api/tasks/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/src/metax_api/tasks/refdata/__init__.py b/src/metax_api/tasks/refdata/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/src/metax_api/tasks/refdata/refdata_indexer/__init__.py b/src/metax_api/tasks/refdata/refdata_indexer/__init__.py new file mode 100755 index 00000000..74447540 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/src/metax_api/tasks/refdata/refdata_indexer/domain/__init__.py b/src/metax_api/tasks/refdata/refdata_indexer/domain/__init__.py new file mode 100755 index 00000000..74447540 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/domain/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/src/metax_api/tasks/refdata/refdata_indexer/domain/indexable_data.py b/src/metax_api/tasks/refdata/refdata_indexer/domain/indexable_data.py new file mode 100755 index 00000000..89304b0d --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/domain/indexable_data.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from metax_api.tasks.refdata.refdata_indexer.service.service_utils import ( + set_default_label, +) + + +class IndexableData: + """ + Base class for any data that can be indexed into Metax Elasticsearch + """ + + DATA_TYPE_RELATION_TYPE = "relation_type" + DATA_TYPE_ORGANIZATION = "organization" + + def __init__(self, doc_id, doc_type, label, uri, same_as, scheme): + + self.doc_type = doc_type + self.doc_id = self._create_es_document_id(doc_id) + self.label = ( + label # { 'fi': 'value1', 'en': 'value2',..., 'und': 'default_value' } + ) + self.same_as = same_as + self.code = doc_id + + self.scheme = "" + if scheme: + self.scheme = scheme + else: + if self.doc_type != IndexableData.DATA_TYPE_RELATION_TYPE: + self.scheme = "http://uri.suomi.fi/codelist/fairdata/" + self.doc_type + + # Replace quotes with corresponding html entity not to break outbound json + if self.label: + set_default_label(self.label) + else: + self.label = {"und": self.code} + + self.uri = uri if uri else "" + + def to_es_document(self): + return str(self) + + def get_es_document_id(self): + return self.doc_id + + def _create_es_document_id(self, doc_id): + return self.doc_type + "_" + doc_id diff --git a/src/metax_api/tasks/refdata/refdata_indexer/domain/organization_data.py b/src/metax_api/tasks/refdata/refdata_indexer/domain/organization_data.py new file mode 100755 index 00000000..d2268f78 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/domain/organization_data.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +import json + +from metax_api.tasks.refdata.refdata_indexer.domain.indexable_data import IndexableData + + +class OrganizationData(IndexableData): + """ + Model class for organization data that can be indexed into Metax Elasticsearch + """ + + ORG_PURL_BASE_URL = ( + "http://uri.suomi.fi/codelist/fairdata/" + + IndexableData.DATA_TYPE_ORGANIZATION + + "/code/" + ) + + def __init__(self, org_id, label, parent_id="", same_as=[], org_csc="", scheme=""): + + super().__init__( + org_id, + IndexableData.DATA_TYPE_ORGANIZATION, + label, + OrganizationData.ORG_PURL_BASE_URL + org_id, + same_as, + scheme, + ) + self.parent_id = "" + if parent_id: + self.parent_id = self._create_es_document_id(parent_id) + self.org_csc = org_csc + + def __str__(self): + return ( + "{" + '"id":"' + self.get_es_document_id() + '",' + '"code":"' + self.code + '",' + '"type":"' + IndexableData.DATA_TYPE_ORGANIZATION + '",' + '"uri":"' + self.uri + '",' + '"org_csc":"' + self.org_csc + '",' + '"parent_id":"' + self.parent_id + '",' + '"label":' + json.dumps(self.label) + "," + '"same_as":' + json.dumps(self.same_as) + "," + '"scheme":"' + self.scheme + '"' + "}" + ) diff --git a/src/metax_api/tasks/refdata/refdata_indexer/domain/reference_data.py b/src/metax_api/tasks/refdata/refdata_indexer/domain/reference_data.py new file mode 100755 index 00000000..132904fc --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/domain/reference_data.py @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +import json + +from metax_api.tasks.refdata.refdata_indexer.domain.indexable_data import IndexableData + + +class ReferenceData(IndexableData): + """ + Model class for reference data that can be indexed into Metax Elasticsearch + """ + + DATA_TYPE_FIELD_OF_SCIENCE = "field_of_science" + DATA_TYPE_LANGUAGE = "language" + DATA_TYPE_LOCATION = "location" + DATA_TYPE_KEYWORD = "keyword" + + DATA_TYPE_RESEARCH_INFRA = "research_infra" + + DATA_TYPE_MIME_TYPE = "mime_type" + + DATA_TYPE_ACCESS_TYPE = "access_type" + DATA_TYPE_RESOURCE_TYPE = "resource_type" + DATA_TYPE_IDENTIFIER_TYPE = "identifier_type" + DATA_TYPE_RESTRICTION_GROUNDS = "restriction_grounds" + DATA_TYPE_CONTRIBUTOR_ROLE = "contributor_role" + DATA_TYPE_CONTRIBUTOR_TYPE = "contributor_type" + DATA_TYPE_FUNDER_TYPE = "funder_type" + DATA_TYPE_LICENSES = "license" + DATA_TYPE_FILE_TYPE = "file_type" + DATA_TYPE_USE_CATEGORY = "use_category" + DATA_TYPE_LIFECYCLE_EVENT = "lifecycle_event" + DATA_TYPE_PRESERVATION_EVENT = "preservation_event" + DATA_TYPE_FILE_FORMAT_VERSION = "file_format_version" + DATA_TYPE_EVENT_OUTCOME = "event_outcome" + + FINTO_REF_DATA_TYPES = [ + DATA_TYPE_FIELD_OF_SCIENCE, + DATA_TYPE_LANGUAGE, + DATA_TYPE_LOCATION, + DATA_TYPE_KEYWORD, + ] + + LOCAL_REF_DATA_TYPES = [ + DATA_TYPE_ACCESS_TYPE, + DATA_TYPE_RESOURCE_TYPE, + DATA_TYPE_IDENTIFIER_TYPE, + DATA_TYPE_RESTRICTION_GROUNDS, + DATA_TYPE_CONTRIBUTOR_ROLE, + DATA_TYPE_CONTRIBUTOR_TYPE, + DATA_TYPE_FUNDER_TYPE, + DATA_TYPE_LICENSES, + DATA_TYPE_FILE_TYPE, + DATA_TYPE_USE_CATEGORY, + IndexableData.DATA_TYPE_RELATION_TYPE, + DATA_TYPE_LIFECYCLE_EVENT, + DATA_TYPE_PRESERVATION_EVENT, + DATA_TYPE_FILE_FORMAT_VERSION, + DATA_TYPE_EVENT_OUTCOME, + DATA_TYPE_RESEARCH_INFRA, + ] + + def __init__( + self, + data_id, + data_type, + label, + uri, + parent_ids=[], + child_ids=[], + same_as=[], + wkt="", + input_file_format="", + output_format_version="", + scheme="", + internal_code="", + ): + + super(ReferenceData, self).__init__( + data_id, data_type, label, uri, same_as, scheme + ) + + self.wkt = wkt + self.input_file_format = input_file_format + self.output_format_version = output_format_version + + self.parent_ids = [] + self.child_ids = [] + self.has_children = False + + self.internal_code = internal_code + + if len(parent_ids) > 0: + self.parent_ids = [self._create_es_document_id(p_id) for p_id in parent_ids] + if len(child_ids) > 0: + self.child_ids = [self._create_es_document_id(c_id) for c_id in child_ids] + + if len(child_ids) > 0: + self.has_children = True + + def __str__(self): + return ( + "{" + '"id":"' + self.get_es_document_id() + '",' + '"code":"' + self.code + '",' + '"type":"' + self.doc_type + '",' + '"uri":"' + self.uri + '",' + '"wkt":"' + self.wkt + '",' + '"input_file_format":"' + self.input_file_format + '",' + '"output_format_version":"' + self.output_format_version + '",' + '"label":' + json.dumps(self.label) + "," + '"parent_ids":' + json.dumps(self.parent_ids) + "," + '"child_ids":' + json.dumps(self.child_ids) + "," + '"has_children":' + json.dumps(self.has_children) + "," + '"same_as":' + json.dumps(self.same_as) + "," + '"internal_code":"' + self.internal_code + '",' + '"scheme":"' + self.scheme + '"' + "}" + ) diff --git a/src/metax_api/tasks/refdata/refdata_indexer/es_index_data.py b/src/metax_api/tasks/refdata/refdata_indexer/es_index_data.py new file mode 100755 index 00000000..583779b0 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/es_index_data.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +import logging.config + +from metax_api.tasks.refdata.refdata_indexer.domain.indexable_data import ( + IndexableData as IdxData, +) +from metax_api.tasks.refdata.refdata_indexer.domain.reference_data import ( + ReferenceData as RefData, +) +from metax_api.tasks.refdata.refdata_indexer.service.elasticsearch_service import ( + ElasticSearchService as ESS, +) +from metax_api.tasks.refdata.refdata_indexer.service.finto_data_service import ( + FintoDataService, +) +# from service.infra_data_service import InfraDataService +from metax_api.tasks.refdata.refdata_indexer.service.local_data_service import ( + LocalDataService, +) +from metax_api.tasks.refdata.refdata_indexer.service.mime_data_service import ( + MimeDataService, +) +from metax_api.tasks.refdata.refdata_indexer.service.organization_service import ( + OrganizationService, +) + +_logger = logging.getLogger(__name__) + + +def index_data(): + """ + Runner file for indexing data to elasticsearch. Make sure requirementx.txt is installed via pip. + """ + NO = "no" + ALL = "all" + + instructions = """ + \nRun the program as metax-user with pyenv activated using + 'python es_index_data.py remove_and_recreate_index=INDEX types_to_reindex=TYPE', + where either or both of the arguments should be provided with one of the following values + per argument:\n\nINDEX:\n{indices}\n\nTYPE:\n{types} + """ + instructions = instructions.format( + indices=str([NO, ALL, ESS.REF_DATA_INDEX_NAME, ESS.ORG_DATA_INDEX_NAME]), + types=str( + [NO, ALL, ESS.REF_DATA_INDEX_NAME, IdxData.DATA_TYPE_ORGANIZATION] + + RefData.FINTO_REF_DATA_TYPES + + RefData.LOCAL_REF_DATA_TYPES + + [RefData.DATA_TYPE_RESEARCH_INFRA, RefData.DATA_TYPE_MIME_TYPE] + ), + ) + + es = ESS() + finto_service = FintoDataService() + local_service = LocalDataService() + org_service = OrganizationService() + mime_service = MimeDataService() + + es.delete_index(ESS.REF_DATA_INDEX_NAME) + es.delete_index(ESS.ORG_DATA_INDEX_NAME) + + # Create reference data index with mappings + + es.create_index(ESS.REF_DATA_INDEX_NAME, ESS.REF_DATA_INDEX_FILENAME) + es.create_index(ESS.ORG_DATA_INDEX_NAME, ESS.ORG_DATA_INDEX_FILENAME) + + # Reindexing for Finto data + + for data_type in RefData.FINTO_REF_DATA_TYPES: + finto_es_data_models = finto_service.get_data(data_type) + if len(finto_es_data_models) == 0: + _logger.info( + "No data models to reindex for finto data type {0}".format(data_type) + ) + continue + + es.delete_and_update_indexable_data( + ESS.REF_DATA_INDEX_NAME, data_type, finto_es_data_models + ) + + # Reindexing local data + + for data_type in RefData.LOCAL_REF_DATA_TYPES: + es.delete_and_update_indexable_data( + ESS.REF_DATA_INDEX_NAME, data_type, local_service.get_data(data_type) + ) + + # Reindexing organizations + + es.delete_and_update_indexable_data( + ESS.ORG_DATA_INDEX_NAME, IdxData.DATA_TYPE_ORGANIZATION, org_service.get_data() + ) + + # Reindexing mime types + + mime_es_data_models = mime_service.get_data() + if len(mime_es_data_models) > 0: + es.delete_and_update_indexable_data( + ESS.REF_DATA_INDEX_NAME, RefData.DATA_TYPE_MIME_TYPE, mime_es_data_models + ) + else: + _logger.info("no data models to reindex for mime type data type") + + _logger.info("Done") diff --git a/src/metax_api/tasks/refdata/refdata_indexer/logconf.json b/src/metax_api/tasks/refdata/refdata_indexer/logconf.json new file mode 100755 index 00000000..abf4d12f --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/logconf.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "disable_existing_loggers": false, + "formatters": { + "standard": { + "format": "%(asctime)s p%(process)d %(name)s %(levelname)s: %(message)s", + "datefmt": "%Y-%m-%dT%H:%M:%S.%03dZ" + } + }, + "handlers": { + "general": { + "level": "INFO", + "class": "logging.FileHandler", + "filename": "/var/log/refdata_indexer/reindex.log", + "formatter": "standard" + } + }, + "loggers": { + "refdata_indexer": { + "level": "INFO", + "handlers": ["general"] + } + } +} \ No newline at end of file diff --git a/src/metax_api/tasks/refdata/refdata_indexer/organization_csv_parser.py b/src/metax_api/tasks/refdata/refdata_indexer/organization_csv_parser.py new file mode 100755 index 00000000..5a174e55 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/organization_csv_parser.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +""" +A script that parses csv files and returns a list of organization objects for elasticsearch indexing purposes. + +The first row in a csv file defines the keys that are used to parse the +organizations. The following keys are accepted as row headers. Separate column values with commas (,): +org_name_fi,org_name_en,org_name_sv,org_code,unit_main_code,unit_sub_code,unit_name +unit_main_code can be left blank, other fields are required. + +This file is a modified version of the original implementation by Peter Kronström. +""" + +import csv +import json +import logging + +from django.conf import settings + +_logger = logging.getLogger(__name__) + +INPUT_FILES = [settings.ORG_FILE_PATH] +OUTPUT_FILE = "/tmp/metax_organizations.json" + + +def parse_csv(): + root_orgs = {} + output_orgs = [] + + for csvfile in INPUT_FILES: + _logger.info("Now parsing file {}".format(csvfile)) + try: + with open(csvfile, "r") as csv_file: + csv_reader = csv.DictReader(csv_file, delimiter=",", quotechar='"') + for row in csv_reader: + # parse fields in a single row + org_name_fi = row.get("org_name_fi", "") + org_name_en = row.get("org_name_en", "") + org_name_sv = row.get("org_name_sv", "") + org_code = row.get("org_code", "") + # unit_main_code = row.get('unit_main_code', '') + unit_sub_code = row.get("unit_sub_code", "") + unit_name = row.get("unit_name", "").rstrip() + org_isni = row.get("org_isni", "") + org_csc = row.get("org_csc", "") + + if govern(row): + # save parent ids to parent_organizations dict + # and create a root level organization + if org_code not in root_orgs: + root_org_dict = create_organization( + org_code, + org_name_fi, + org_name_en=org_name_en, + org_name_sv=org_name_sv, + org_isni=org_isni, + org_csc=org_csc, + ) + root_orgs[org_code] = root_org_dict.get("org_id", None) + output_orgs.append(root_org_dict) + + # otherwise create an org and append it to existing root's hierarchy + if unit_sub_code and unit_name: + organization_code = "-".join( + [org_code, unit_sub_code] + ) # Unique + parent_id = root_orgs.get(org_code, None) + output_orgs.append( + create_organization( + organization_code, unit_name, parent_id=parent_id + ) + ) + + except IOError: + _logger.error("File {} could not be found.".format(csvfile)) + + with open(OUTPUT_FILE, "w+") as outfile: + json.dump(output_orgs, outfile) + + +def govern(row): + """ + returns false, if the row does not contain necessary fields. + """ + + # root-level organization only + if all(row[i] for i in ["org_name_fi", "org_code"]): + # check if sub-unit fields are present + if not all(row[i] for i in ["unit_sub_code", "unit_name"]): + _logger.error( + f"Missing unit codes (unit_sub_code, unit_name). Creating root organization only: {row}" + ) + return True + else: + _logger.error( + f"Missing root organization fields (org_name_fi, org_code). Skipping row {row}" + ) + return False + + +def create_organization( + org_id_str, + org_name_fi, + org_name_en=None, + org_name_sv=None, + org_isni=None, + org_csc=None, + parent_id=None, +): + """ + create organization data_dict that is suitable for ES indexing + """ + _logger.info("creating organization") + org_dict = {} + org_dict["org_id"] = org_id_str + + org_dict["label"] = {"fi": org_name_fi, "und": org_name_fi} + if org_name_en: + org_dict["label"]["en"] = org_name_en + + if org_name_sv: + org_dict["label"]["sv"] = org_name_sv + + if parent_id: + org_dict["parent_id"] = parent_id + + if org_isni: + org_dict["same_as"] = [org_isni] + + if org_csc: + org_dict["org_csc"] = org_csc + + return org_dict diff --git a/src/metax_api/tasks/refdata/refdata_indexer/requirements.txt b/src/metax_api/tasks/refdata/refdata_indexer/requirements.txt new file mode 100755 index 00000000..b7708b86 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/requirements.txt @@ -0,0 +1,3 @@ +requests +elasticsearch>=7.0.0,<8.0.0 +rdflib diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/es-config/organization_data_index.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/es-config/organization_data_index.json new file mode 100755 index 00000000..bcbd2b10 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/es-config/organization_data_index.json @@ -0,0 +1,57 @@ +{ + "settings" : { + "index" : { + "number_of_shards" : 2, + "number_of_replicas" : 1 + }, + "analysis": { + "analyzer": "standard" + } + }, + "mappings": { + "dynamic_templates": [ + { + "label": { + "path_match": "label.*", + "mapping": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + } + ], + "properties": { + "id": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "uri": { + "type": "keyword" + }, + "org_csc": { + "type": "keyword", + "index": false + }, + "parent_id": { + "type": "keyword" + }, + "same_as": { + "type": "keyword", + "index": false + }, + "scheme": { + "type": "keyword" + } + } + } +} diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/es-config/reference_data_index.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/es-config/reference_data_index.json new file mode 100755 index 00000000..af5184d1 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/es-config/reference_data_index.json @@ -0,0 +1,75 @@ +{ + "settings" : { + "index" : { + "number_of_shards" : 2, + "number_of_replicas" : 1 + }, + "analysis": { + "analyzer": "standard" + } + }, + "mappings": { + "dynamic_templates": [ + { + "label": { + "path_match": "label.*", + "mapping": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + } + ], + "properties": { + "id": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "uri": { + "type": "keyword" + }, + "wkt": { + "type": "keyword" + }, + "input_file_format": { + "type": "keyword" + }, + "output_format_version": { + "type": "keyword" + }, + "parent_ids": { + "type": "keyword", + "index": false + }, + "child_ids": { + "type": "keyword", + "index": false + }, + "has_children": { + "type": "boolean", + "index": false + }, + "same_as": { + "type": "keyword", + "index": false + }, + "scheme": { + "type": "keyword" + }, + "internal_code": { + "type": "keyword", + "index": false + } + } + } +} diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/access_type.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/access_type.json new file mode 100755 index 00000000..b18f59b1 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/access_type.json @@ -0,0 +1,7 @@ +[ +{"id": "open", "uri":"http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "label": {"fi": "Avoin", "en": "Open"}, "same_as":["http://publications.europa.eu/resource/authority/access-right/PUBLIC"]}, +{"id": "login", "uri":"http://uri.suomi.fi/codelist/fairdata/access_type/code/login", "label": {"fi": "Vaatii kirjautumisen Fairdata-palvelussa", "en": "Requires login in Fairdata service"}, "same_as":["http://publications.europa.eu/resource/authority/access-right/RESTRICTED"]}, +{"id": "permit", "uri":"http://uri.suomi.fi/codelist/fairdata/access_type/code/permit", "label": {"fi": "Vaatii luvan hakemista Fairdata-palvelussa", "en": "Requires applying permission in Fairdata service"}, "same_as":["http://publications.europa.eu/resource/authority/access-right/RESTRICTED"], "internal_code": "hide"}, +{"id": "restricted", "uri":"http://uri.suomi.fi/codelist/fairdata/access_type/code/restricted", "label": {"fi": "Saatavuutta rajoitettu", "en": "Restricted use"}, "same_as":["http://publications.europa.eu/resource/authority/access-right/RESTRICTED"]}, +{"id": "embargo", "uri":"http://uri.suomi.fi/codelist/fairdata/access_type/code/embargo", "label": {"fi": "Embargo", "en": "Embargo"}, "same_as":["http://publications.europa.eu/resource/authority/access-right/NON_PUBLIC"]} +] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/contributor_role.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/contributor_role.json new file mode 100755 index 00000000..543dc220 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/contributor_role.json @@ -0,0 +1,14 @@ +[{"id":"conceptualization","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization","label":{"en":"Conceptualization", "fi":"Tutkimuksen muotoilu", "sv":"Formulering av forskningen"}}, +{"id":"data_curation","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/data_curation","label":{"en":"Data curation", "fi": "Datan käsittely", "sv":"Databehandling"}}, +{"id":"formal_analysis","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/formal_analysis","label":{"en":"Formal analysis", "fi":"Tiedon analysointi", "sv": "Formell analys"}}, +{"id":"funding_acquisition","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition","label":{"en":"Funding acquisition", "fi":"Rahoituksen hankinta", "sv": "Anskaffande av finansiering"}}, +{"id":"investigation","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/investigation","label":{"en":"Investigation", "fi":"Tutkimus", "sv":"Undersökning"}}, +{"id":"methodology","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/methodology","label":{"en":"Methodology", "fi":"Menetelmät", "sv":"Metod"}}, +{"id":"project_administration","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/project_administration","label":{"en":"Project administration", "fi":"Projektin hallinta", "sv":"Projekthantering"}}, +{"id":"resources","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/resources","label":{"en":"Resources", "fi":"Aineiston hankinta", "sv":"Material"}}, +{"id":"software","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/software","label":{"en":"Software", "fi":"Sovelluskehitys", "sv":"Mjukvara"}}, +{"id":"supervision","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/supervision","label":{"en":"Supervision", "fi":"Ohjaus", "sv":"Handledning"}}, +{"id":"validation","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/validation","label":{"en":"Validation", "fi":"Validointi", "sv":"Validering"}}, +{"id":"visualization","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization","label":{"en":"Visualization", "fi":"Visualisointi", "sv":"Visualisering"}}, +{"id":"writing_original","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/writing_original","label":{"en":"Writing - original draft", "fi":"Kirjoitus - alkuperäinen luonnos", "sv":"Skrivit - urprungligt utkast"}}, +{"id":"writing_editing","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_role/code/writing_editing","label":{"en":"Writing - review & editing", "fi":"Kirjoitus - tarkistus ja muokkaus", "sv":"Skrivit - granskning och redigering"}}] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/contributor_type.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/contributor_type.json new file mode 100755 index 00000000..1bc5247e --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/contributor_type.json @@ -0,0 +1,20 @@ +[{"id":"ContactPerson","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson","label":{"en":"Point of contact", "fi":"Yhteystiedot", "sv":"Kontaktuppgifter"}}, +{"id":"DataCollector","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector","label":{"en":"Data Collector", "fi":"Aineiston kerääjä", "sv":"Datainsamling"}}, +{"id":"DataCurator","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator","label":{"en":"Data Curator", "fi":"Aineiston käsittelijä", "sv":"Databehandling"}}, +{"id":"DataManager","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager","label":{"en":"Data Manager", "fi":"Aineiston hallinnoija", "sv":"Dataadministration"}}, +{"id":"Distributor","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor","label":{"en":"Distributor", "fi":"Jakelija", "sv":"Distributör"}}, +{"id":"Editor","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor","label":{"en":"Editor", "fi":"Julkaisutoimittaja", "sv":"Redaktör"}}, +{"id":"HostingInstitution","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution","label":{"en":"Hosting Institution", "fi":"Säilyttävä organisaatio", "sv":"Bevarande organisation"}}, +{"id":"Producer","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Producer","label":{"en":"Producer", "fi":"Tuottaja", "sv":"Producent"}}, +{"id":"ProjectLeader","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader","label":{"en":"Project Leader", "fi":"Projektin johtaja", "sv":"Projektledare"}}, +{"id":"ProjectMember","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectMember","label":{"en":"Project Member", "fi":"Projektin jäsen", "sv":"Projektmedlem"}}, +{"id":"RegistrationAgency","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/RegistrationAgency","label":{"en":"Registration Agency", "fi":"Rekisterinpitäjä", "sv":"Registrator"}}, +{"id":"RegistrationAuthority","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/RegistrationAuthority","label":{"en":"Registration Authority", "fi":"Rekisteriviranomainen", "sv":"Registermyndighet"}}, +{"id":"RelatedPerson","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/RelatedPerson","label":{"en":"Related Person", "fi":"Liittyvä henkilö", "sv":"Relaterad person"}}, +{"id":"Researcher","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Researcher","label":{"en":"Researcher", "fi":"Tutkija", "sv":"Forskare"}}, +{"id":"ResearchGroup","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup","label":{"en":"ResearchGroup", "fi":"Tutkimusryhmä", "sv":"Forskningsgrupp"}}, +{"id":"RightsHolder","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/RightsHolder","label":{"en":"Rights Holder", "fi":"Oikeuksienhaltija", "sv":"Rättighetsinnehavare"}}, +{"id":"Sponsor","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Sponsor","label":{"en":"Sponsor", "fi":"Sponsori", "sv":"Sponsor"}}, +{"id":"Supervisor","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Supervisor","label":{"en":"Supervisor", "fi":"Työn ohjaaja", "sv":"Handledare"}}, +{"id":"WorkPackageLeader","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/WorkPackageLeader","label":{"en":"Work Package Leader", "fi":"Työpaketin vetäjä", "sv":"Ledare för arbetspaket"}}, +{"id":"Other","uri":"http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Other","label":{"en":"Other", "fi":"Muu", "sv":"Annan"}}] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/event_outcome.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/event_outcome.json new file mode 100755 index 00000000..827b81e1 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/event_outcome.json @@ -0,0 +1,3 @@ +[{"id":"Success","uri":"http://uri.suomi.fi/codelist/fairdata/event_outcome/code/success","label":{"en":"Success", "fi":"Onnistunut", "sv":"Framgångsrik"}}, +{"id":"Failure","uri":"http://uri.suomi.fi/codelist/fairdata/event_outcome/code/failure","label":{"en":"Failure", "fi":"Epäonnistunut", "sv":"Misslyckad"}}, +{"id":"Unknown","uri":"http://uri.suomi.fi/codelist/fairdata/event_outcome/code/unknown","label":{"en":"Unknown", "fi":"Tuntematon", "sv":"Okänt"}}] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_format_version.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_format_version.json new file mode 100755 index 00000000..a7140c51 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_format_version.json @@ -0,0 +1,100 @@ +[ + {"id": "file_format_version_text_csv", "input_file_format": "text/csv", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/text_csv"}, + {"id": "file_format_version_application_epub+zip_2.0.1", "input_file_format": "application/epub+zip", "output_format_version": "2.0.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_epub+zip_2.0.1"}, + {"id": "file_format_version_application_epub+zip_3.0.0", "input_file_format": "application/epub+zip", "output_format_version": "3.0.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_epub+zip_3.0.0"}, + {"id": "file_format_version_application_epub+zip_3.0.1", "input_file_format": "application/epub+zip", "output_format_version": "3.0.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_epub+zip_3.0.1"}, + {"id": "file_format_version_application_epub+zip_3.1", "input_file_format": "application/epub+zip", "output_format_version": "3.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_epub+zip_3.1"}, + {"id": "file_format_version_application_xhtml+xml_1.0", "input_file_format": "application/xhtml+xml", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_xhtml+xml_1.0"}, + {"id": "file_format_version_application_xhtml+xml_1.1", "input_file_format": "application/xhtml+xml", "output_format_version": "1.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_xhtml+xml_1.1"}, + {"id": "file_format_version_text_xml_1.0", "input_file_format": "text/xml", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/text_xml_1.0"}, + {"id": "file_format_version_text_html_4.01", "input_file_format": "text/html", "output_format_version": "4.01", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/text_html_4.01"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.text_1.0", "input_file_format": "application/vnd.oasis.opendocument.text", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.text_1.0"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.text_1.1", "input_file_format": "application/vnd.oasis.opendocument.text", "output_format_version": "1.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.text_1.1"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.text_1.2", "input_file_format": "application/vnd.oasis.opendocument.text", "output_format_version": "1.2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.text_1.2"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.spreadsheet_1.0", "input_file_format": "application/vnd.oasis.opendocument.spreadsheet", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.spreadsheet_1.0"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.spreadsheet_1.1", "input_file_format": "application/vnd.oasis.opendocument.spreadsheet", "output_format_version": "1.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.spreadsheet_1.1"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.spreadsheet_1.2", "input_file_format": "application/vnd.oasis.opendocument.spreadsheet", "output_format_version": "1.2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.spreadsheet_1.2"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.presentation_1.0", "input_file_format": "application/vnd.oasis.opendocument.presentation", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.presentation_1.0"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.presentation_1.1", "input_file_format": "application/vnd.oasis.opendocument.presentation", "output_format_version": "1.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.presentation_1.1"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.presentation_1.2", "input_file_format": "application/vnd.oasis.opendocument.presentation", "output_format_version": "1.2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.presentation_1.2"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.graphics_1.0", "input_file_format": "application/vnd.oasis.opendocument.graphics", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.graphics_1.0"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.graphics_1.1", "input_file_format": "application/vnd.oasis.opendocument.graphics", "output_format_version": "1.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.graphics_1.1"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.graphics_1.2", "input_file_format": "application/vnd.oasis.opendocument.graphics", "output_format_version": "1.2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.graphics_1.2"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.formula_1.0", "input_file_format": "application/vnd.oasis.opendocument.formula", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.formula_1.0"}, + {"id": "file_format_version_application_vnd.oasis.opendocument.formula_1.2", "input_file_format": "application/vnd.oasis.opendocument.formula", "output_format_version": "1.2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.oasis.opendocument.formula_1.2"}, + {"id": "file_format_version_application_pdf_A-1a", "input_file_format": "application/pdf", "output_format_version": "A-1a", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_A-1a"}, + {"id": "file_format_version_application_pdf_A-1b", "input_file_format": "application/pdf", "output_format_version": "A-1b", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_A-1b"}, + {"id": "file_format_version_application_pdf_A-2a", "input_file_format": "application/pdf", "output_format_version": "A-2a", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_A-2a"}, + {"id": "file_format_version_application_pdf_A-2b", "input_file_format": "application/pdf", "output_format_version": "A-2b", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_A-2b"}, + {"id": "file_format_version_application_pdf_A-2u", "input_file_format": "application/pdf", "output_format_version": "A-2u", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_A-2u"}, + {"id": "file_format_version_application_pdf_A-3a", "input_file_format": "application/pdf", "output_format_version": "A-3a", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_A-3a"}, + {"id": "file_format_version_application_pdf_A-3b", "input_file_format": "application/pdf", "output_format_version": "A-3b", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_A-3b"}, + {"id": "file_format_version_application_pdf_A-3u", "input_file_format": "application/pdf", "output_format_version": "A-3u", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_A-3u"}, + {"id": "file_format_version_text_plain", "input_file_format": "text/plain", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/text_plain"}, + {"id": "file_format_version_audio_x-aiff_1.3", "input_file_format": "audio/x-aiff", "output_format_version": "1.3", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_x-aiff_1.3"}, + {"id": "file_format_version_audio_x-wav_2", "input_file_format": "audio/x-wav", "output_format_version": "2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_x-wav_2"}, + {"id": "file_format_version_audio_flac_1.2.1", "input_file_format": "audio/flac", "output_format_version": "1.2.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_flac_1.2.1"}, + {"id": "file_format_version_audio_mp4", "input_file_format": "audio/mp4", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_mp4"}, + {"id": "file_format_version_audio_x-wav", "input_file_format": "audio/x-wav", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_x-wav"}, + {"id": "file_format_version_image_x-dpx_2.0", "input_file_format": "image/x-dpx", "output_format_version": "2.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_x-dpx_2.0"}, + {"id": "file_format_version_video_jpeg2000", "input_file_format": "video/jpeg2000", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/video_jpeg2000"}, + {"id": "file_format_version_video_mp4", "input_file_format": "video/mp4", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/video_mp4"}, + {"id": "file_format_version_image_tiff_1.3", "input_file_format": "image/tiff", "output_format_version": "1.3", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_tiff_1.3"}, + {"id": "file_format_version_image_tiff_1.4", "input_file_format": "image/tiff", "output_format_version": "1.4", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_tiff_1.4"}, + {"id": "file_format_version_image_jpeg_1.00", "input_file_format": "image/jpeg", "output_format_version": "1.00", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_jpeg_1.00"}, + {"id": "file_format_version_image_jpeg_1.01", "input_file_format": "image/jpeg", "output_format_version": "1.01", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_jpeg_1.01"}, + {"id": "file_format_version_image_jpeg_1.02", "input_file_format": "image/jpeg", "output_format_version": "1.02", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_jpeg_1.02"}, + {"id": "file_format_version_image_jpeg_2.0", "input_file_format": "image/jpeg", "output_format_version": "2.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_jpeg_2.0"}, + {"id": "file_format_version_image_jpeg_2.1", "input_file_format": "image/jpeg", "output_format_version": "2.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_jpeg_2.1"}, + {"id": "file_format_version_image_jpeg_2.2", "input_file_format": "image/jpeg", "output_format_version": "2.2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_jpeg_2.2"}, + {"id": "file_format_version_image_jpeg_2.2.1", "input_file_format": "image/jpeg", "output_format_version": "2.2.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_jpeg_2.2.1"}, + {"id": "file_format_version_image_jp2", "input_file_format": "image/jp2", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_jp2"}, + {"id": "file_format_version_image_tiff_6.0", "input_file_format": "image/tiff", "output_format_version": "6.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_tiff_6.0"}, + {"id": "file_format_version_image_png_1.2", "input_file_format": "image/png", "output_format_version": "1.2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_png_1.2"}, + {"id": "file_format_version_application_x-internet-archive_1.0", "input_file_format": "application/x-internet-archive", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_x-internet-archive_1.0"}, + {"id": "file_format_version_application_x-internet-archive_1.1", "input_file_format": "application/x-internet-archive", "output_format_version": "1.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_x-internet-archive_1.1"}, + {"id": "file_format_version_application_warc_0.17", "input_file_format": "application/warc", "output_format_version": "0.17", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_warc_0.17"}, + {"id": "file_format_version_application_warc_1.0", "input_file_format": "application/warc", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_warc_1.0"}, + {"id": "file_format_version_image_tiff_1.0", "input_file_format": "image/tiff", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_tiff_1.0"}, + {"id": "file_format_version_application_gml+xml_3.2.1", "input_file_format": "application/gml+xml", "output_format_version": "3.2.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_gml+xml_3.2.1"}, + {"id": "file_format_version_application_vnd.google-earth.kml+xml_2.3", "input_file_format": "application/vnd.google-earth.kml+xml", "output_format_version": "2.3", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.google-earth.kml+xml_2.3"}, + {"id": "file_format_version_application_x-spss-por", "input_file_format": "application/x-spss-por", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_x-spss-por"}, + {"id": "file_format_version_application_msword_8.0", "input_file_format": "application/msword", "output_format_version": "8.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_msword_8.0"}, + {"id": "file_format_version_application_msword_8.5", "input_file_format": "application/msword", "output_format_version": "8.5", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_msword_8.5"}, + {"id": "file_format_version_application_msword_9.0", "input_file_format": "application/msword", "output_format_version": "9.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_msword_9.0"}, + {"id": "file_format_version_application_msword_10.0", "input_file_format": "application/msword", "output_format_version": "10.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_msword_10.0"}, + {"id": "file_format_version_application_msword_11.0", "input_file_format": "application/msword", "output_format_version": "11.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_msword_11.0"}, + {"id": "file_format_version_application_vnd.openxmlformats-officedocument.wordprocessingml.document_12.0", "input_file_format": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "output_format_version": "12.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.wordprocessingml.document_12.0"}, + {"id": "file_format_version_application_vnd.openxmlformats-officedocument.wordprocessingml.document_14.0", "input_file_format": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "output_format_version": "14.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.wordprocessingml.document_14.0"}, + {"id": "file_format_version_application_vnd.openxmlformats-officedocument.wordprocessingml.document_15.0", "input_file_format": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "output_format_version": "15.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.wordprocessingml.document_15.0"}, + {"id": "file_format_version_application_vnd.ms-excel_8.0", "input_file_format": "application/vnd.ms-excel", "output_format_version": "8.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-excel_8.0"}, + {"id": "file_format_version_application_vnd.ms-excel_9.0", "input_file_format": "application/vnd.ms-excel", "output_format_version": "9.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-excel_9.0"}, + {"id": "file_format_version_application_vnd.ms-excel_10.0", "input_file_format": "application/vnd.ms-excel", "output_format_version": "10.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-excel_10.0"}, + {"id": "file_format_version_application_vnd.ms-excel_11.0", "input_file_format": "application/vnd.ms-excel", "output_format_version": "11.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-excel_11.0"}, + {"id": "file_format_version_application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_12.0", "input_file_format": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "output_format_version": "12.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_12.0"}, + {"id": "file_format_version_application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_14.0", "input_file_format": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "output_format_version": "14.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_14.0"}, + {"id": "file_format_version_application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_15.0", "input_file_format": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "output_format_version": "15.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_15.0"}, + {"id": "file_format_version_application_vnd.ms-powerpoint_8.0", "input_file_format": "application/vnd.ms-powerpoint", "output_format_version": "8.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-powerpoint_8.0"}, + {"id": "file_format_version_application_vnd.ms-powerpoint_9.0", "input_file_format": "application/vnd.ms-powerpoint", "output_format_version": "9.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-powerpoint_9.0"}, + {"id": "file_format_version_application_vnd.ms-powerpoint_10.0", "input_file_format": "application/vnd.ms-powerpoint", "output_format_version": "10.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-powerpoint_10.0"}, + {"id": "file_format_version_application_vnd.ms-powerpoint_11.0", "input_file_format": "application/vnd.ms-powerpoint", "output_format_version": "11.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-powerpoint_11.0"}, + {"id": "file_format_version_application_vnd.openxmlformats-officedocument.presentationml.presentation_12.0", "input_file_format": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "output_format_version": "12.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.presentationml.presentation_12.0"}, + {"id": "file_format_version_application_vnd.openxmlformats-officedocument.presentationml.presentation_14.0", "input_file_format": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "output_format_version": "14.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.presentationml.presentation_14.0"}, + {"id": "file_format_version_application_vnd.openxmlformats-officedocument.presentationml.presentation_15.0", "input_file_format": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "output_format_version": "15.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.presentationml.presentation_15.0"}, + {"id": "file_format_version_application_pdf_1.2", "input_file_format": "application/pdf", "output_format_version": "1.2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.2"}, + {"id": "file_format_version_application_pdf_1.3", "input_file_format": "application/pdf", "output_format_version": "1.3", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.3"}, + {"id": "file_format_version_application_pdf_1.4", "input_file_format": "application/pdf", "output_format_version": "1.4", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.4"}, + {"id": "file_format_version_application_pdf_1.5", "input_file_format": "application/pdf", "output_format_version": "1.5", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.5"}, + {"id": "file_format_version_application_pdf_1.6", "input_file_format": "application/pdf", "output_format_version": "1.6", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.6"}, + {"id": "file_format_version_application_pdf_1.7", "input_file_format": "application/pdf", "output_format_version": "1.7", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.7"}, + {"id": "file_format_version_audio_x-aiff", "input_file_format": "audio/x-aiff", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_x-aiff"}, + {"id": "file_format_version_audio_mpeg", "input_file_format": "audio/mpeg", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_mpeg"}, + {"id": "file_format_version_audio_x-ms-wma_9", "input_file_format": "audio/x-ms-wma", "output_format_version": "9", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_x-ms-wma_9"}, + {"id": "file_format_version_video_dv", "input_file_format": "video/dv", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/video_dv"}, + {"id": "file_format_version_video_mpeg_1", "input_file_format": "video/mpeg", "output_format_version": "1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/video_mpeg_1"}, + {"id": "file_format_version_video_mpeg_2", "input_file_format": "video/mpeg", "output_format_version": "2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/video_mpeg_2"}, + {"id": "file_format_version_video_x-ms-wmv_9", "input_file_format": "video/x-ms-wmv", "output_format_version": "9", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/video_x-ms-wmv_9"}, + {"id": "file_format_version_application_postscript_3.0", "input_file_format": "application/postscript", "output_format_version": "3.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_postscript_3.0"}, + {"id": "file_format_version_image_gif_1987a", "input_file_format": "image/gif", "output_format_version": "1987a", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_gif_1987a"}, + {"id": "file_format_version_image_gif_1989a", "input_file_format": "image/gif", "output_format_version": "1989a", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_gif_1989a"} +] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_type.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_type.json new file mode 100755 index 00000000..c64a6797 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_type.json @@ -0,0 +1,9 @@ +[{"id":"audiovisual", "uri":"http://uri.suomi.fi/codelist/fairdata/file_type/code/audiovisual", "label":{"fi":"Audiovisuaalinen", "en":"Audiovisual"}}, + {"id":"video", "uri":"http://uri.suomi.fi/codelist/fairdata/file_type/code/video", "label":{"fi":"Video", "en":"Video"}}, + {"id":"image", "uri":"http://uri.suomi.fi/codelist/fairdata/file_type/code/image", "label":{"fi":"Kuva","en":"Image"},"same_as":["http://purl.org/dc/dcmitype/Image"]}, + {"id":"source_code", "uri":"http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code", "label":{"fi":"Lähdekoodi","en":"Source code"}}, + {"id":"software", "uri":"http://uri.suomi.fi/codelist/fairdata/file_type/code/software", "label":{"fi":"Sovellus","en":"Software"},"same_as":["http://purl.org/dc/dcmitype/Software"]}, + {"id":"sound", "uri":"http://uri.suomi.fi/codelist/fairdata/file_type/code/sound", "label":{"fi":"Ääni","en":"Sound"},"same_as":["http://purl.org/dc/dcmitype/Sound"]}, + {"id":"text", "uri":"http://uri.suomi.fi/codelist/fairdata/file_type/code/text", "label":{"fi":"Teksti","en":"Text"},"same_as":["http://purl.org/dc/dcmitype/Text"]}, + {"id":"binary", "uri":"http://uri.suomi.fi/codelist/fairdata/file_type/code/binary", "label":{"fi":"Binääri","en":"Binary"}}, + {"id":"other", "uri":"http://uri.suomi.fi/codelist/fairdata/file_type/code/other", "label":{"fi":"Muu","en":"Other"}}] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/funder_type.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/funder_type.json new file mode 100755 index 00000000..67e37f64 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/funder_type.json @@ -0,0 +1,13 @@ +[ + {"id": "academy-of-finland", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/academy-of-finland", "label": {"fi": "Suomen Akatemia", "en": "Academy of Finland"}}, + {"id": "tekes", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/tekes", "label": {"fi": "Tekes", "en": "Tekes"}}, + {"id": "tekes-shok", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/tekes-shok", "label": {"fi": "Tekes SHOK", "en": "Tekes SHOK"}}, + {"id": "eu-framework-programme", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/eu-framework-programme", "label": {"fi": "EU puiteohjelmat", "en": "EU Framework Programme"}}, + {"id": "eu-eakr", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/eu-eakr", "label": {"fi": "EU Euroopan aluekehitysrahasto EAKR", "en": "EU Regional Development Fund EAKR"}}, + {"id": "eu-esr", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/eu-esr", "label": {"fi": "EU Euroopan sosiaalirahasto ESR", "en": "EU European Social Fund ESR"}}, + {"id": "eu-other", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/eu-other", "label": {"fi": "EU muu rahoitus", "en": "EU other funding"}}, + {"id": "finnish-fof", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/finnish-fof", "label": {"fi": "Kotimainen rahasto tai säätiö", "en": "Finnish fund or foundation"}}, + {"id": "foreign-fof", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/foreign-fof", "label": {"fi": "Ulkomainen rahasto tai säätiö", "en": "Foreign fund or foundation"}}, + {"id": "other-public", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/other-public", "label": {"fi": "Muu julkinen rahoitus", "en": "Other public funding"}}, + {"id": "commercial", "uri":"http://uri.suomi.fi/codelist/fairdata/funder_type/code/commercial", "label": {"fi": "Yritys", "en": "Commercial"}} +] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/identifier_type.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/identifier_type.json new file mode 100755 index 00000000..f8a31162 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/identifier_type.json @@ -0,0 +1,21 @@ +[{"id":"ark","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/ark","label":{"en":"Archival Resource Key (ARK)"},"same_as":["http://purl.org/spar/datacite/ark"]}, +{"id":"doi","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/doi","label":{"en":"Digital Object Identifier (DOI)"},"same_as":["http://purl.org/spar/datacite/doi"]}, +{"id":"bibcode","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/bibcode","label":{"en":"Astrophysics Data System Code(bibcode)"},"same_as":["http://purl.org/spar/datacite/bibode"]}, +{"id":"ean13","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/ean13","label":{"en":"European Article Number (EAN13)"},"same_as":["http://purl.org/spar/datacite/ean13"]}, +{"id":"arxiv","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/arxiv","label":{"en":"arXiv identifer"},"same_as":["http://purl.org/spar/datacite/arxiv"]}, +{"id":"eissn","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/eissn","label":{"en":"Electronic International Standard Serial Number (EISSN)"},"same_as":["http://purl.org/spar/datacite/eissn"]}, +{"id":"handle","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/handle","label":{"en":"Handle - an abstact reference to a resource"},"same_as":["http://purl.org/spar/datacite/handle"]}, +{"id":"igsn","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/igsn","label":{"en":"International Geo Sample Number (IGSN)"}}, +{"id":"isbn","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/isbn","label":{"en":"International Standard Book Number (ISBN)"},"same_as":["http://purl.org/spar/datacite/isbn"]}, +{"id":"issn","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/issn","label":{"en":"International Standard Serial Number (ISSN)"},"same_as":["http://purl.org/spar/datacite/issn"]}, +{"id":"istc","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/istc","label":{"en":"International Standard Text Code (ISTC)"},"same_as":["http://purl.org/spar/datacite/istc"]}, +{"id":"lissn","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/lissn","label":{"en":"The linking ISSN"},"same_as":["http://purl.org/spar/datacite/lissn"]}, +{"id":"lsid","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/lsid","label":{"en":"Life Science Identifier (LSID)"},"same_as":["http://purl.org/spar/datacite/lsid"]}, +{"id":"pmid","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/pmid","label":{"en":"PubMed Identifier (PMID)"},"same_as":["http://purl.org/spar/datacite/pmid"]}, +{"id":"purl","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/purl","label":{"en":"Persistent Uniform Resource Locator (PURL)"},"same_as":["http://purl.org/spar/datacite/purl"]}, +{"id":"upc","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/upc","label":{"en":"Universal Product Code (UPC)"},"same_as":["http://purl.org/spar/datacite/upc"]}, +{"id":"url","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/url","label":{"en":"Universal Resource Locator (URL)"},"same_as":["http://purl.org/spar/datacite/url"]}, +{"id":"uri","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/uri","label":{"en":"Universal Resource Identifier (URI)"},"same_as":["http://purl.org/spar/datacite/uri"]}, +{"id":"urn","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/urn","label":{"en":"Uniform Resource Name (URN)"},"same_as":["http://purl.org/spar/datacite/urn"]}, +{"id":"orcid","uri":"http://uri.suomi.fi/codelist/fairdata/identifier_type/code/orcid","label":{"en":"Open Researcher and Contributor Identifier (ORCID)"},"same_as":["http://purl.org/spar/datacite/orcid"]} +] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/license.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/license.json new file mode 100755 index 00000000..7e7a6e33 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/license.json @@ -0,0 +1,308 @@ +[{ + "id": "CC0-1.0", + "label": { + "fi": "Creative Commons Yleismaailmallinen (CC0 1.0) Public Domain -lausuma", + "en": "Creative Commons CC0 1.0 Universal (CC0 1.0) Public Domain Dedication" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC0-1.0", + "same_as": ["https://creativecommons.org/publicdomain/zero/1.0/"] + }, + { + "id": "CC-BY-4.0", + "label": { + "fi": "Creative Commons Nimeä 4.0 Kansainvälinen (CC BY 4.0)", + "en": "Creative Commons Attribution 4.0 International (CC BY 4.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0", + "same_as": ["https://creativecommons.org/licenses/by/4.0/"] + }, + { + "id": "CC-BY-SA-4.0", + "label": { + "fi": "Creative Commons Nimeä-JaaSamoin 4.0 Kansainvälinen (CC BY-SA 4.0)", + "en": "Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-SA-4.0", + "same_as": ["https://creativecommons.org/licenses/by-sa/4.0/"] + }, + { + "id": "CC-BY-NC-4.0", + "label": { + "fi": "Creative Commons Nimeä-EiKaupallinen 4.0 Kansainvälinen (CC BY-NC 4.0)", + "en": "Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-4.0", + "same_as": ["https://creativecommons.org/licenses/by-nc/4.0/"] + }, + { + "id": "ODC-PDDL-1.0", + "label": { + "en": "Open Data Commons Public Domain Dedication and License 1.0" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/ODC-PDDL-1.0", + "same_as": ["http://www.opendefinition.org/licenses/odc-pddl"] + }, + { + "id": "ODbL-1.0", + "label": { + "en": "Open Data Commons Open Database License 1.0" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/ODbL-1.0", + "same_as": ["http://www.opendefinition.org/licenses/odc-odbl"] + }, + { + "id": "ODC-BY-1.0", + "label": { + "en": "Open Data Commons Attribution License 1.0" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/ODC-BY-1.0", + "same_as": ["http://www.opendefinition.org/licenses/odc-by"] + }, + { + "id": "GFDL-1.3-no-cover-texts-no-invariant-sections", + "label": { + "en": "GNU Free Documentation License 1.3 with no cover texts and no invariant sections" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/GFDL-1.3-no-cover-texts-no-invariant-sections", + "same_as": ["http://www.opendefinition.org/licenses/gfdl"] + }, + { + "id": "CC-BY-3.0", + "label": { + "fi": "Creative Commons Nimeä 3.0 Ei sovitettu (CC BY 3.0)", + "en": "Creative Commons Attribution 3.0 Unported (CC BY 3.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-3.0", + "same_as": ["https://creativecommons.org/licenses/by/3.0/"] + }, + { + "id": "CC-BY-SA-3.0", + "label": { + "fi": "Creative Commons Nimeä-JaaSamoin 3.0 Ei sovitettu (CC BY-SA 3.0)", + "en": "Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-SA-3.0", + "same_as": ["https://creativecommons.org/licenses/by-sa/3.0/"] + }, + { + "id": "CC-BY-NC-3.0", + "label": { + "fi": "Creative Commons Nimeä-EiKaupallinen 3.0 Ei sovitettu (CC BY-NC 3.0)", + "en": "Creative Commons Attribution-NonCommercial 3.0 Unported (CC BY-NC 3.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-3.0", + "same_as": ["https://creativecommons.org/licenses/by-nc/3.0/"] + }, + { + "id": "CC-BY-2.0", + "label": { + "fi": "Creative Commons Nimeä 2.0 Yleinen (CC BY 2.0)", + "en": "Creative Commons Attribution 2.0 Generic (CC BY 2.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-2.0", + "same_as": ["https://creativecommons.org/licenses/by/2.0/"] + }, + { + "id": "CC-BY-SA-2.0", + "label": { + "fi": "Creative Commons Nimeä-JaaSamoin 2.0 Yleinen (CC BY-SA 2.0)", + "en": "Creative Commons Attribution-ShareAlike 2.0 Generic (CC BY-SA 2.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-SA-2.0", + "same_as": ["https://creativecommons.org/licenses/by-sa/2.0/"] + }, + { + "id": "CC-BY-NC-2.0", + "label": { + "fi": "Creative Commons Nimeä-EiKaupallinen 2.0 Yleinen (CC BY-NC 2.0)", + "en": "Creative Commons Attribution-NonCommercial 2.0 Generic (CC BY-NC 2.0" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-2.0", + "same_as": ["https://creativecommons.org/licenses/by-nc/2.0/"] + }, + { + "id": "CC-BY-1.0", + "label": { + "fi": "Creative Commons Nimeä 1.0 Yleinen (CC BY 1.0)", + "en": "Creative Commons Attribution 1.0 Generic (CC BY 1.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-1.0", + "same_as": ["https://creativecommons.org/licenses/by/1.0/"] + }, + { + "id": "CC-BY-SA-1.0", + "label": { + "en": "Creative Commons Attribution-ShareAlike 1.0 Generic (CC BY-SA 1.0)", + "fi": "Creative Commons Nimeä-JaaSamoin 1.0 Yleinen (CC BY-SA 1.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-SA-1.0", + "same_as": ["https://creativecommons.org/licenses/by-sa/1.0/"] + }, + { + "id": "CC-BY-NC-1.0", + "label": { + "en": "Creative Commons Attribution-NonCommercial 1.0 Generic (CC BY-NC 1.0)", + "fi": "Creative Commons Nimeä-EiKaupallinen 1.0 Yleinen (CC BY-NC 1.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-1.0", + "same_as": ["https://creativecommons.org/licenses/by-nc/1.0/"] + }, + { + "id": "Apache-2.0", + "label": { + "en": "Apache Software License 2.0" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", + "same_as": ["http://www.opensource.org/licenses/Apache-2.0"] + }, + { + "id": "ClarinPUB-1.0", + "label": { + "en": "CLARIN PUB (Public) End-User License 1.0" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/ClarinPUB-1.0", + "same_as": ["https://kitwiki.csc.fi/twiki/bin/view/FinCLARIN/ClarinEulaPub"] + }, + { + "id": "ClarinACA-1.0", + "label": { + "en": "CLARIN ACA (Academic) End-User License 1.0" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/ClarinACA-1.0", + "same_as": ["https://kitwiki.csc.fi/twiki/bin/view/FinCLARIN/ClarinEulaAca"] + }, + { + "id": "ClarinACA+NC-1.0", + "label": { + "en": "CLARIN ACA+NC (Academic, Non-Commercial) End-User License 1.0" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/ClarinACA+NC-1.0", + "same_as": ["https://kitwiki.csc.fi/twiki/bin/view/FinCLARIN/ClarinEulaAcaNCDep"] + }, + { + "id": "ClarinRES-1.0", + "label": { + "en": "CLARIN RES (Restricted) End-User License 1.0" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/ClarinRES-1.0", + "same_as": ["https://kitwiki.csc.fi/twiki/bin/view/FinCLARIN/ClarinEulaRes"] + }, + { + "id": "CC-BY-ND-3.0", + "label": { + "en": "Creative Commons Attribution-NoDerivs 3.0 Unported (CC BY-ND 3.0)", + "fi": "Creative Commons Nimeä-EiMuutoksia 3.0 Ei sovitettu (CC BY-ND 3.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-ND-3.0", + "same_as": ["https://creativecommons.org/licenses/by-nd/3.0/"] + }, + { + "id": "CC-BY-ND-4.0", + "label": { + "en": "Creative Commons Attribution-NoDerivatives 4.0 International (CC BY-ND 4.0)", + "fi": "Creative Commons Nimeä-EiMuutoksia 4.0 Kansainvälinen (CC BY-ND 4.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-ND-4.0", + "same_as": ["https://creativecommons.org/licenses/by-nd/4.0/"] + }, + { + "id": "CC-BY-NC-SA-3.0", + "label": { + "en": "Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0)", + "fi": "Creative Commons Nimeä-EiKaupallinen-JaaSamoin 3.0 Ei sovitettu (CC BY-NC-SA 3.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-SA-3.0", + "same_as": ["https://creativecommons.org/licenses/by-nc-sa/3.0/"] + }, + { + "id": "CC-BY-NC-SA-4.0", + "label": { + "en": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)", + "fi": "Creative Commons Nimeä-EiKaupallinen-JaaSamoin 4.0 Kansainvälinen (CC BY-NC-SA 4.0) " + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-SA-4.0", + "same_as": ["https://creativecommons.org/licenses/by-nc-sa/4.0/"] + }, + { + "id": "CC-BY-NC-ND-3.0", + "label": { + "en": "Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported (CC BY-NC-ND 3.0)", + "fi": "Creative Commons Nimeä-EiKaupallinen-EiMuutoksia 3.0 Ei sovitettu (CC BY-NC-ND 3.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-ND-3.0", + "same_as": ["https://creativecommons.org/licenses/by-nc-nd/3.0/"] + }, + { + "id": "CC-BY-NC-ND-4.0", + "label": { + "en": "Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)", + "fi": "Creative Commons Nimeä-EiKaupallinen-EiMuutoksia 4.0 Kansainvälinen (CC BY-NC-ND 4.0)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-ND-4.0", + "same_as": ["https://creativecommons.org/licenses/by-nc-nd/4.0/"] + }, + { + "id": "other", + "label": { + "fi": "Muu", + "en": "Other" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/other" + }, + { + "id": "other-open", + "label": { + "fi": "Muu (Avoin)", + "en": "Other (Open)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/other-open" + }, + { + "id": "other-pd", + "label": { + "fi": "Muu (Public Domain)", + "en": "Other (Public Domain)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/other-pd" + }, + { + "id": "other-at", + "label": { + "fi": "Muu (Nimeä)", + "en": "Other (Attribution)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/other-at" + }, + { + "id": "other-nc", + "label": { + "fi": "Muu (Ei kaupallinen)", + "en": "Other (Non-Commercial)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/other-nc" + }, + { + "id": "other-closed", + "label": { + "fi": "Muu (Ei avoin)", + "en": "Other (Not Open)" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/other-closed" + }, + { + "id": "notspecified", + "label": { + "fi": "Ei määritelty", + "en": "License Not Specified" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/notspecified" + }, + { + "id": "undernegotiation", + "label": { + "fi": "Neuvottelut kesken", + "en": "Under negotiation" + }, + "uri": "http://uri.suomi.fi/codelist/fairdata/license/code/undernegotiation" + } +] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/lifecycle_event.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/lifecycle_event.json new file mode 100755 index 00000000..b16f8571 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/lifecycle_event.json @@ -0,0 +1,16 @@ +[{"id":"ordered", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/ordered", "label":{"fi":"Tilattu","en":"Ordered"}}, +{"id":"planned", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/planned", "label":{"fi":"Suunniteltu","en":"Planned"}}, +{"id":"generated", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/generated", "label":{"fi":"Tuotettu","en":"Generated"}}, +{"id":"collected", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", "label":{"fi":"Kerätty","en":"Collected"}}, +{"id":"received", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/received", "label":{"fi":"Vastaanotettu","en":"Received"}}, +{"id":"checked", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/checked", "label":{"fi":"Tarkistettu","en":"Checked"}}, +{"id":"fixed", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/fixed", "label":{"fi":"Korjattu","en":"Fixed"}}, +{"id":"modified", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/modified", "label":{"fi":"Muokattu","en":"Modified"}}, +{"id":"transferred", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/transferred", "label":{"fi":"Siirretty","en":"Transferred"}}, +{"id":"reused", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/reused", "label":{"fi":"Käytetty","en":"Re-used"}}, +{"id":"cited", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/cited", "label":{"fi":"Viitattu","en":"Cited"}}, +{"id":"archived", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/archived", "label":{"fi":"Arkistoitu","en":"Archived"}}, +{"id":"curated", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/curated", "label":{"fi":"Kuratoitu","en":"Curated"}}, +{"id":"removed", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/removed", "label":{"fi":"Poistettu","en":"Removed"}}, +{"id":"destroyed", "uri":"http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/destroyed", "label":{"fi":"Tuhottu","en":"Destroyed"}} +] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/preservation_event.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/preservation_event.json new file mode 100755 index 00000000..ea1d73b6 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/preservation_event.json @@ -0,0 +1,21 @@ +[ +{"id":"com", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/com", "label":{"fi":"Pakkaus","en":"Compression"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/com"]}, +{"id":"cre", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/cre", "label":{"fi":"Luonti","en":"Creation"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/cre"]}, +{"id":"dea", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/dea", "label":{"fi":"Käytön estäminen","en":"Deaccession"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/dea"]}, +{"id":"dec", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/dec", "label":{"fi":"Pakkauksen purkaminen","en":"Decompression"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/dec"]}, +{"id":"der", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/der", "label":{"fi":"Salauksen purkaminen","en":"Decryption"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/der"]}, +{"id":"del", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/del", "label":{"fi":"Poistaminen","en":"Deletion"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/del"]}, +{"id":"mes", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/mes", "label":{"fi":"Tarkistussumman laskeminen","en":"Message digest calculation"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/mes"]}, +{"id":"dig", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/dig", "label":{"fi":"Allekirjoituksen validointi","en":"Digital signature validation"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/dig"]}, +{"id":"fix", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/fix", "label":{"fi":"Eheyden tarkistus","en":"Fixity check"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/fix"]}, +{"id":"ing", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/ing", "label":{"fi":"Säilytykseen vastaanottaminen","en":"Ingestion"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/ing"]}, +{"id":"upd", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd", "label":{"fi":"Objektin päivitys","en":"Object update"}}, +{"id":"mod", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/mod", "label":{"fi":"Metatiedon päivitys","en":"Metadata modification"}}, +{"id":"mig", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/mig", "label":{"fi":"Migrointi","en":"Migration"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/mig"]}, +{"id":"rsp", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/rsp", "label":{"fi":"Säilytysvastuun siirto","en":"Preservation responsibility change"}}, +{"id":"pro", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/pro", "label":{"fi":"Prosessointi","en":"Processing"}}, +{"id":"rep", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/rep", "label":{"fi":"Replikointi","en":"Replication"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/rep"]}, +{"id":"val", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/val", "label":{"fi":"Validointi","en":"Validation"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/val"]}, +{"id":"vir", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/vir", "label":{"fi":"Virustarkistus","en":"Virus check"}, "same_as":["http://id.loc.gov/vocabulary/preservation/eventType/vir"]}, +{"id":"cge", "uri":"http://uri.suomi.fi/codelist/fairdata/preservation_event/code/cge", "label":{"fi":"Tiedostonimen muutos","en":"Filename change"}} +] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/relation_type.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/relation_type.json new file mode 100755 index 00000000..66401c9e --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/relation_type.json @@ -0,0 +1,14 @@ +[{"id":"relation", "uri":"http://purl.org/dc/terms/relation", "label":{"fi":"Liittyvä aineisto", "en":"Related dataset"}}, +{"id":"cites", "uri":"http://purl.org/spar/cito/cites", "label":{"fi":"Viittaa", "en":"Cites"}}, +{"id":"isCitedBy", "uri":"http://purl.org/spar/cito/isCitedBy", "label":{"fi":"Viitattu", "en":"Is cited by"}}, +{"id":"isSupplementTo", "uri":"http://purl.org/vocab/frbr/core#isSupplementTo", "label":{"fi":"Viittaus tausta-aineistoon", "en":"Is supplement to"}}, +{"id":"continues", "uri":"http://purl.org/vocab/frbr/core#successorOf", "label":{"fi":"Täydentää aineistoa", "en":"Continues"}}, +{"id":"hasPreviousVersion", "uri":"http://www.w3.org/ns/adms#previous", "label":{"fi":"Edellinen versio", "en":"Has previous version"}}, +{"id":"hasNextVersion", "uri":"http://www.w3.org/ns/adms#next", "label":{"fi":"Seuraava versio", "en":"Has next version"}}, +{"id":"hasPart", "uri":"http://purl.org/dc/terms/hasPart", "label":{"fi":"Osa aineistoa", "en":"Has part"}}, +{"id":"isPartOf", "uri":"http://purl.org/dc/terms/isPartOf", "label":{"fi":"Kuuluu aineistoon", "en":"Is part of"}}, +{"id":"references", "uri":"http://purl.org/spar/cito/citesForInformation", "label":{"fi":"Viittaa aineistoon", "en":"References"}}, +{"id":"isCompiledBy", "uri":"http://purl.org/spar/cito/isCompiledBy", "label":{"fi":"Johdettu jollakin", "en":"Is compiled by"}}, +{"id":"alternate", "uri":"http://purl.org/vocab/frbr/core#alternate", "label":{"fi":"Erimuotoinen vastine", "en":"Is variant form of"}}, +{"id":"isIdenticalTo", "uri":"http://www.w3.org/2002/07/owl#sameAs", "label":{"fi":"Identtinen aineisto", "en":"Is identical to"}}, +{"id":"wasDerivedFrom", "uri":"http://www.w3.org/ns/prov#wasDerivedFrom", "label":{"fi":"Johdettu aineistosta", "en":"Was derived from"}}] \ No newline at end of file diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/research_infra.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/research_infra.json new file mode 100755 index 00000000..d9fd5959 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/research_infra.json @@ -0,0 +1,50 @@ +[ +{"id":"urn-nbn-fi-research-infras-2016111647","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111647","label":{"fi": "Pohjoismainen optinen teleskooppi", "en": "Nordic Optical Telescope", "und": "Pohjoismainen optinen teleskooppi"}}, +{"id":"urn-nbn-fi-research-infras-201607252","uri":"http://urn.fi/urn:nbn:fi:research-infras-201607252","label":{"fi": "Yhteinen kieliaineistojen ja -teknologian infrastruktuuri", "en": "Common Language Resource and Technology Infrastructure", "und": "Yhteinen kieliaineistojen ja -teknologian infrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-201607254","uri":"http://urn.fi/urn:nbn:fi:research-infras-201607254","label":{"fi": "Kansallisen Digitaalisen Kirjaston asiakasliittym\u00e4", "en": "National Digital Library public interface", "und": "Kansallisen Digitaalisen Kirjaston asiakasliittym\u00e4"}}, +{"id":"urn-nbn-fi-research-infras-201607255","uri":"http://urn.fi/urn:nbn:fi:research-infras-201607255","label":{"fi": "Kansallinen rekisteri- ja mikroaineistojen tutkijapalvelu", "en": "Finnish Microdata Access Services", "und": "Kansallinen rekisteri- ja mikroaineistojen tutkijapalvelu"}}, +{"id":"urn-nbn-fi-research-infras-201607258","uri":"http://urn.fi/urn:nbn:fi:research-infras-201607258","label":{"fi": "EISCAT_3D Suomi ja ISR-tutkaj\u00e4rjestelm\u00e4", "und": "EISCAT_3D Suomi ja ISR-tutkaj\u00e4rjestelm\u00e4"}}, +{"id":"urn-nbn-fi-research-infras-2016072516","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072516","label":{"fi": "Euroopan translationaalinen tutkimuksen infrastruktuuri", "en": "European infrastructure for translational medicine", "und": "Euroopan translationaalinen tutkimuksen infrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072517","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072517","label":{"fi": "Euroopan luonnontieteiden infrastruktuuri biologiselle tieteelle", "en": "European distributed infrastructure for life-science information", "und": "Euroopan luonnontieteiden infrastruktuuri biologiselle tieteelle"}}, +{"id":"urn-nbn-fi-research-infras-2016072520","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072520","label":{"fi": "Euroopan hiirimallien analysoinnin, s\u00e4ilytt\u00e4misen ja jakelun tutkimusinfrastruktuuri", "en": "INFRAFRONTIER Research Infrastructure", "und": "Euroopan hiirimallien analysoinnin, s\u00e4ilytt\u00e4misen ja jakelun tutkimusinfrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072524","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072524","label":{"fi": "MAX IV Laboratorio - Synkrotonis\u00e4teilylaitos", "en": "MAX IV Laboratory", "und": "MAX IV Laboratorio - Synkrotonis\u00e4teilylaitos"}}, +{"id":"urn-nbn-fi-research-infras-2016072528","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072528","label":{"fi": "Cherenkov teleskooppij\u00e4rjestelm\u00e4", "en": "Cherenkov Telescope Array", "und": "Cherenkov teleskooppij\u00e4rjestelm\u00e4"}}, +{"id":"urn-nbn-fi-research-infras-2016072530","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072530","label":{"fi": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio", "en": "Accelerator laboratory of the university of Jyv\u00e4skyl\u00e4", "und": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio"}}, +{"id":"urn-nbn-fi-research-infras-2016072531","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072531","label":{"fi": "CSC - Tieteen tietotekniikan keskus -kansallinen tutkimusinfrastruktuuri", "en": "CSC - IT Center for Science -national research infrastructure", "und": "CSC - Tieteen tietotekniikan keskus -kansallinen tutkimusinfrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072533","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072533","label":{"fi": "Suomen hila- ja pilvilaskennan infrastruktuuri", "en": "Finnish Grid and Cloud Infrastructure", "und": "Suomen hila- ja pilvilaskennan infrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016111634","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111634","label":{"fi": "Euroopan synkrotronis\u00e4teilytutkimuslaitos", "en": "European Synchrotron Radiation Facility", "und": "Euroopan synkrotronis\u00e4teilytutkimuslaitos"}}, +{"id":"urn-nbn-fi-research-infras-2016111636","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111636","label":{"fi": "Suomen Lajitietokeskus", "en": "Finnish Biodiversity Information Facility", "und": "Suomen Lajitietokeskus"}}, +{"id":"urn-nbn-fi-research-infras-2016111637","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111637","label":{"fi": "Maailman Lajitietokeskus", "en": "Global Biodiversity Information Facility", "und": "Maailman Lajitietokeskus"}}, +{"id":"urn-nbn-fi-research-infras-2016111638","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111638","label":{"fi": "Euroopan molekyylibiologian laboratorio", "en": "European Molecular Biology Laboratory", "und": "Euroopan molekyylibiologian laboratorio"}}, +{"id":"urn-nbn-fi-research-infras-2016111641","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111641","label":{"en": "RawMatTERS Finland Infrastructure", "und": "RawMatTERS Finland Infrastructure"}}, +{"id":"urn-nbn-fi-research-infras-2016111643","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111643","label":{"fi": "Euroopan etel\u00e4inen observatorio", "en": "European Southern Observatory", "und": "Euroopan etel\u00e4inen observatorio"}}, +{"id":"urn-nbn-fi-research-infras-2016111648","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111648","label":{"fi": "Pohjoismaiden e-infrastuktuuri yhteisty\u00f6", "en": "Nordic e-Infrastructure Collaboration", "und": "Pohjoismaiden e-infrastuktuuri yhteisty\u00f6"}}, +{"id":"urn-nbn-fi-research-infras-201607251","uri":"http://urn.fi/urn:nbn:fi:research-infras-201607251","label":{"fi": "Eurooppalainen sosiaalitutkimus", "en": "European Social Survey", "und": "Eurooppalainen sosiaalitutkimus"}}, +{"id":"urn-nbn-fi-research-infras-201607253","uri":"http://urn.fi/urn:nbn:fi:research-infras-201607253","label":{"fi": "Kansallinen elektroninen kirjasto", "en": "National Electronic Library", "und": "Kansallinen elektroninen kirjasto"}}, +{"id":"urn-nbn-fi-research-infras-201607256","uri":"http://urn.fi/urn:nbn:fi:research-infras-201607256","label":{"fi": "Yhteiskuntatieteellinen tietoarkisto ja Euroopan tietoarkistojen konsortio", "en": "Finnish Social Science Data Archive and Consortium of European Social Science Data Archives", "und": "Yhteiskuntatieteellinen tietoarkisto ja Euroopan tietoarkistojen konsortio"}}, +{"id":"urn-nbn-fi-research-infras-201607257","uri":"http://urn.fi/urn:nbn:fi:research-infras-201607257","label":{"fi": "Kansalliset tietoinfrastruktuuripalvelut ATT ja KDK-PAS", "en": "National information infrastructure by Open Science and Research Initiative and National Digital Library", "und": "Kansalliset tietoinfrastruktuuripalvelut ATT ja KDK-PAS"}}, +{"id":"urn-nbn-fi-research-infras-201607259","uri":"http://urn.fi/urn:nbn:fi:research-infras-201607259","label":{"fi": "Euroopan geotieteiden infrastruktuuri", "en": "Finnish national initiative of the European Plate Observing System", "und": "Euroopan geotieteiden infrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072510","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072510","label":{"fi": "Suomen merentutkimuksen infrastruktuuri", "en": "Finnish Marine Research Infrastructure", "und": "Suomen merentutkimuksen infrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072511","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072511","label":{"fi": "Integroitu kasvihuonekaasujen havainnointij\u00e4rjestelm\u00e4", "en": "Integrated Carbon Observation System", "und": "Integroitu kasvihuonekaasujen havainnointij\u00e4rjestelm\u00e4"}}, +{"id":"urn-nbn-fi-research-infras-2016072512","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072512","label":{"fi": "Ilmakeh\u00e4- ja ymp\u00e4rist\u00f6tutkimuksen tutkimusinfrastruktuuri", "en": "Integrated atmoshperic and earth system science research infrastructure", "und": "Ilmakeh\u00e4- ja ymp\u00e4rist\u00f6tutkimuksen tutkimusinfrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072513","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072513","label":{"fi": "Avoin paikkatiedon tutkimusinfrastruktuuri", "en": "Open Geospatial Information Infrastructure for Research", "und": "Avoin paikkatiedon tutkimusinfrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072514","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072514","label":{"fi": "Biopankki-infrastruktuuri", "en": "Biobanking and biomolecular resources research infrastructure", "und": "Biopankki-infrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072515","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072515","label":{"fi": "Biokeskus Suomi", "en": "Biocenter Finland", "und": "Biokeskus Suomi"}}, +{"id":"urn-nbn-fi-research-infras-2016072518","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072518","label":{"en": "European Research Infrastructure for Biomedical Imaging (Euro-BioImaging)", "und": "European Research Infrastructure for Biomedical Imaging (Euro-BioImaging)"}}, +{"id":"urn-nbn-fi-research-infras-2016072519","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072519","label":{"en": "European Infrastructure of Open Screening Platforms for Chemical Biology", "und": "European Infrastructure of Open Screening Platforms for Chemical Biology"}}, +{"id":"urn-nbn-fi-research-infras-2016072521","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072521","label":{"fi": "Integroidun rakennebiologian tutkimusinfrastruktuuri", "en": "Integrated Structural Biology Research Infrastructure", "und": "Integroidun rakennebiologian tutkimusinfrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072522","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072522","label":{"fi": "Kansallinen kasvien fenotyyppaus -infrastruktuuri", "und": "Kansallinen kasvien fenotyyppaus -infrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072523","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072523","label":{"fi": "Kansallinen virusvektorilaboratorio", "en": "National Virus Vector Laboratory", "und": "Kansallinen virusvektorilaboratorio"}}, +{"id":"urn-nbn-fi-research-infras-2016072525","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072525","label":{"fi": "Otaniemen mikro- ja nanoteknologioiden tutkimusinfrastruktuuri", "en": "Otaniemi Research Infrastructure for Micro and Nanotechnologies", "und": "Otaniemen mikro- ja nanoteknologioiden tutkimusinfrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072526","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072526","label":{"fi": "Eurooppalainen r\u00f6ntgen-vapaaelektronilaser ja biologinen infrastruktuuri", "en": "X-ray free-electron laser (XFEL) Biological Infrastructure", "und": "Eurooppalainen r\u00f6ntgen-vapaaelektronilaser ja biologinen infrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016072527","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072527","label":{"fi": "Huippuallianssi kest\u00e4v\u00e4\u00e4n biomassan jalostukseen", "en": "Bioeconomy Infrastructure", "und": "Huippuallianssi kest\u00e4v\u00e4\u00e4n biomassan jalostukseen"}}, +{"id":"urn-nbn-fi-research-infras-2016072529","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072529","label":{"fi": "Euclid-kosmologiamissio", "en": "Euclid cosmology mission", "und": "Euclid-kosmologiamissio"}}, +{"id":"urn-nbn-fi-research-infras-2016072532","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016072532","label":{"fi": "PRACE, Eurooppalainen suurteholaskentainfrastruktuuri", "en": "Partnership for Advanced Computing in Europe", "und": "PRACE, Eurooppalainen suurteholaskentainfrastruktuuri"}}, +{"id":"urn-nbn-fi-research-infras-2016111635","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111635","label":{"fi": "Kansainv\u00e4linen merellisen kuoren ja merenpohjan tutkimusohjelma (IODP)", "en": "International Ocean Discovery Program", "und": "Kansainv\u00e4linen merellisen kuoren ja merenpohjan tutkimusohjelma (IODP)"}}, +{"id":"urn-nbn-fi-research-infras-2016111639","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111639","label":{"fi": "Tutkimusinfrastruktuuri yksitt\u00e4isten solujen analysointiin", "en": "Infrastructure for Single Cell Capture and Processing", "und": "Tutkimusinfrastruktuuri yksitt\u00e4isten solujen analysointiin"}}, +{"id":"urn-nbn-fi-research-infras-2016111642","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111642","label":{"fi": "Heliumionimikroskooppi poikkitieteelliseen materiaalitutkimukseen", "en": "Helium Ion Microscope for Interdisciplinary Materials Research", "und": "Heliumionimikroskooppi poikkitieteelliseen materiaalitutkimukseen"}}, +{"id":"urn-nbn-fi-research-infras-2016111644","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111644","label":{"fi": "Seuraavan sukupolven termohydraulinen koelaite", "en": "Next Generation Thermal Hydraulic Test Loop", "und": "Seuraavan sukupolven termohydraulinen koelaite"}}, +{"id":"urn-nbn-fi-research-infras-2016111645","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111645","label":{"fi": "Euroopan hiukkasfysiikan tutkimuslaitos", "en": "European Organization for Nuclear Research", "und": "Euroopan hiukkasfysiikan tutkimuslaitos"}}, +{"id":"urn-nbn-fi-research-infras-2016111646","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111646","label":{"fi": "Ydinfysiikan kansainv\u00e4linen kiihdytinlaboratorio", "en": "Facility for Antiproton and Ion Research", "und": "Ydinfysiikan kansainv\u00e4linen kiihdytinlaboratorio"}}, +{"id":"urn-nbn-fi-research-infras-2016111647","uri":"http://urn.fi/urn:nbn:fi:research-infras-2016111647","label":{"fi": "Pohjoismainen optinen teleskooppi", "en": "Nordic Optical Telescope", "und": "Pohjoismainen optinen teleskooppi"}} +] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/resource_type.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/resource_type.json new file mode 100755 index 00000000..a0d2df31 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/resource_type.json @@ -0,0 +1,17 @@ +[{"id":"api", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/api", "label":{"fi":"Rajapinta","en":"Application interface"},"same_as":["http://datacite.org/schema/kernel-4#Software"]}, + {"id":"ui", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui", "label":{"fi":"Käyttöliittymä","en":"User interface"},"same_as":["http://datacite.org/schema/kernel-4#Software"]}, + {"id":"service", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/service", "label":{"fi":"Palvelu","en":"Service"},"same_as":["http://purl.org/dc/dcmitype/Service","http://datacite.org/schema/kernel-4#Service"]}, + {"id":"software", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/software", "label":{"fi":"Sovellus","en":"Software"},"same_as":["http://datacite.org/schema/kernel-4#Software"]}, + {"id":"model", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/model", "label":{"fi":"Mallinnus","en":"Model"},"same_as":["http://datacite.org/schema/kernel-4#Model"]}, + {"id":"collection", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection", "label":{"fi":"Kokoelma","en":"Collection"},"same_as":["http://purl.org/dc/dcmitype/Collection","http://datacite.org/schema/kernel-4#Collection"]}, + {"id":"physical_object", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object", "label":{"fi":"Fyysinen objekti","en":"Physical object"},"same_as":["http://purl.org/dc/dcmitype/PhysicalObject","http://datacite.org/schema/kernel-4#PhysicalObject"]}, + {"id":"instrument", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/instrument", "label":{"fi":"Mittalaite","en":"Instrument"},"same_as":["http://purl.org/dc/dcmitype/PhysicalObject","http://datacite.org/schema/kernel-4#PhysicalObject"]}, + {"id":"dataset", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/dataset", "label":{"fi":"Tutkimusaineisto","en":"Dataset"},"same_as":["http://datacite.org/schema/kernel-4#Dataset"]}, + {"id":"audiovisual", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/audiovisual", "label":{"fi":"Audiovisuaalinen esitys","en":"Audiovisual"},"same_as":["http://datacite.org/schema/kernel-4#Audiovisual"]}, + {"id":"image", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/image", "label":{"fi":"Kuva","en":"Image"},"same_as":["http://datacite.org/schema/kernel-4#Image"]}, + {"id":"sound", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/sound", "label":{"fi":"Ääni","en":"Sound"},"same_as":["http://datacite.org/schema/kernel-4#Sound"]}, + {"id":"publication", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/publication", "label":{"fi":"Julkaisu","en":"Publication"},"same_as":["http://datacite.org/schema/kernel-4#Text"]}, + {"id":"text", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/text", "label":{"fi":"Teksti","en":"Text"},"same_as":["http://datacite.org/schema/kernel-4#Text"]}, + {"id":"event", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/event", "label":{"fi":"Tapahtuma","en":"Event"}, "same_as":["http://datacite.org/schema/kernel-4#Event"]}, + {"id":"workflow", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/workflow", "label":{"fi":"Työnkulun dokumentaatio","en":"Workflow"},"same_as":["http://datacite.org/schema/kernel-4#Workflow"]}, + {"id":"other", "uri":"http://uri.suomi.fi/codelist/fairdata/resource_type/code/other", "label":{"fi":"Muu","en":"Other"},"same_as":["http://datacite.org/schema/kernel-4#Other"]}] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/restriction_grounds.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/restriction_grounds.json new file mode 100755 index 00000000..54651ee0 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/restriction_grounds.json @@ -0,0 +1,13 @@ +[ +{"id": "contractual", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/contractual", "label": {"fi": "Saatavuutta rajoitettu sopimuksen perusteella, esim. luottamuksellisen kaupallisen tai teollisen toiminnan perusteella", "en": "Restricted access due to contractual reasons, eg. commercial or industrial use","sv": "Begränsad åtkomst på grund av kontrakt"}}, +{"id": "personal_data", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/personal_data", "label": {"fi": "Saatavuutta rajoitettu luottamuksellisiin henkilötietoihin perustuen","en": "Restricted access due to personal data content","sv": "Begränsad åtkomst på grund av personuppgifter"}}, +{"id": "personal_interest", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/personal_interest", "label": {"fi": "Saatavuutta rajoitettu tietoja antaneen henkilön etuun tai suojaan perustuen tai esim. luovutussopimuksen perusteella","en": "Restricted access based on the interest or protection of the person who provided the information or, for example, on the basis of extradite","sv": "Begränsad åtkomst på grund av skydd av person"}}, +{"id": "national_interest", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/national_interest", "label": {"fi": "Saatavuutta rajoitettu kansainvälisiin suhteisiin, yleiseen turvallisuuteen tai kansalliseen puolustukseen perustuen","en": "Restricted access due to international relations, public security or national defense","sv": "Begränsad åtkomst på grund av nationellt intresse eller säkerhet"}}, +{"id": "judicial", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/judicial", "label": {"fi": "Saatavuutta rajoitettu tuomioistuinkäsittelyn tai oikeudenkäyntiin perustuen","en": "Restricted access due to on-going court proceedings","sv": "Begränsad åtkomst på grund av rättegång"}}, +{"id": "environmental", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/environmental", "label": {"fi": "Saatavuutta rajoitettu luonnonsuojelun perusteella","en": "Restricted access due to environmental preservation reasons","sv": "Begränsad åtkomst på grund av miljöskydd"}}, +{"id": "copyright", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/copyright", "label": {"fi": "Saatavuutta rajoitettu tekijäoikeuden perusteella","en": "Restricted access due to copyright","sv": "Begränsad åtkomst på grund av upphovsrätt"}}, +{"id": "cultural", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/cultural", "label": {"fi": "Saatavuutta rajoitettu kulttuuriperinnön tai alkuperäiskansojen suojelun perusteella", "en": "Restricted access for protection of cultural heritage or First Nations","sv": "Begränsad åtkomst på grund av skydd av kulturarv"}}, +{"id": "research", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/research", "label": {"fi": "Saatavuutta rajoitettu sopimuksen perusteella vain tutkimuskäyttöön", "en": "Restriced access for research based on contract", "sv": "Begränsad åtkomst på bas av kontrakt ändast för forskningsändamål"}}, +{"id": "education", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/education", "label": {"fi": "Saatavuutta rajoitettu sopimuksen perusteella opetukseen ja opiskeluun", "en": "Restricted access for teaching or studying based on contract", "sv": "Begränsad åtkomst på bas av kontrakt ändast för undervisning och studier"}}, +{"id": "other", "uri": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other", "label": {"fi": "Saatavuutta rajoitettu muulla perusteella", "en": "Restricted access due to other reasons","sv": "Begränsad åtkomst av övriga skäl"}} +] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/use_category.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/use_category.json new file mode 100755 index 00000000..95cca7ed --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/use_category.json @@ -0,0 +1,8 @@ +[{"id":"source", "uri":"http://uri.suomi.fi/codelist/fairdata/use_category/code/source", "label":{"fi":"Lähdeaineisto","en":"Source material"}}, +{"id":"outcome", "uri":"http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome", "label":{"fi":"Tulosaineisto","en":"Outcome material"}}, +{"id":"publication", "uri":"http://uri.suomi.fi/codelist/fairdata/use_category/code/publication", "label":{"fi":"Julkaisu","en":"Publication"}}, +{"id":"documentation", "uri":"http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation", "label":{"fi":"Dokumentaatio","en":"Documentation"}}, +{"id":"configuration", "uri":"http://uri.suomi.fi/codelist/fairdata/use_category/code/configuration", "label":{"fi":"Konfiguraatiotiedosto","en":"Configuration files"}}, +{"id":"method", "uri":"http://uri.suomi.fi/codelist/fairdata/use_category/code/method", "label":{"fi":"Metodi","en":"Method"}}, +{"id":"rights", "uri":"http://uri.suomi.fi/codelist/fairdata/use_category/code/rights", "label":{"fi":"Oikeuksien kuvaus","en":"Rights statement"}} +] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv b/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv new file mode 100755 index 00000000..933af772 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv @@ -0,0 +1,2440 @@ +org_name_fi,org_name_en,org_name_sv,org_code,unit_main_code,unit_sub_code,unit_name,org_isni,org_csc +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,,,http://isni.org/isni/0000000108389418,2 +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A800,"School services, ARTS",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A801,"Department of Film, Television and Scenography",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A802,Department of Media,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A803,Department of Design,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A805,Department of Art,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A850,Aalto Future Media Center,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A899,School Common ARTS,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E700,"School Services, BIZ",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E701,Department of Accounting,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E702,Department of Marketing,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E703,Department of Economics,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E704,Department of Information and Service Economy,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E706,Department of Management Studies,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E707,Department of Finance,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E710,Center for Markets in Transition (CEMAT),, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E720,Center for Knowledge and Innovation Research (CKIR),, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E790,"School Common, BIZ",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T100,"School services, CHEM",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T101,Department of Biotechnology and Chemical Technology,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T102,Department of Chemistry,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T103,Department of Materials Science and Engineering,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T104,Department of Forest Products Technology,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T105,Department of Chemistry and Materials Science,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T106,Department of Chemical and Metallurgical Engineering,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T107,Department of Bioproducts and Biosystems,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T199,"Shool common, CHEM",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T200,"School cervices, ENG",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T201,Department of Architecture,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T210,Design Factory,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T211,"School common, ENG",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T212,Department of Mechanical Engineering,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T213,Department of Built Environment,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T214,Department of Civil Engineering,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T300,School services SCI,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T302,Department of Mathematics and Systems Analysis,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T304,Department of Applied Physics,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T307,Department of Industrial Engineering and Management,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T310,EIT Digital Helsinki,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T313,Department of Computer Science,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T314,Department of Neuroscience and Biomedical Engineering,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T320,"School common, SCI",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T400,School services ELEC,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T403,Deptartment of Micro & Nanoscience,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T404,Department of Radio Science & Engineering,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T405,Department of Signal Processing & Acoustics,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T407,Department of Communications & Networking,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T408,Metsähovi Radio Observatory,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T409,Aalto Nanofab,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T410,Department of Electrical Engineering & Automation,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T411,Department of Electronics & Nanoengineering,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T499,"School common, ELEC",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U100,University level,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U900,"Leadership and LS Support Serv, Joint Units",, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U901,Research and Innovation Serv,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U902,Learning Services,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U905,Finance Services,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U906,HR Services,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U908,Communications Services,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U909,IT Services,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U912,Campus Services,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U915,Secretarial Services,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U917,Fundraising,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U918,Endowment,, +Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U920,Aalto Common Items,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,,,http://isni.org/isni/0000000110165683,3556 +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Liiktal,Liiketalouden koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Kemtek,Kemiantekniikan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Tiettek,Tietotekniikan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Sahtek,Sähkö- ja automaatiotekniikan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Terva,Terveysalan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Sosia,Sosiaalialan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Musa,Musiikin koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Huma,Humanistisen ja kasvatusalan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,YAMK,Ylemmät AMK-tutkinnot,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Korkpal,Korkeakoulupalvelut,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,TKIpal,TKI-palvelut,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Tuotal,Tuotantotalouden koulutus,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,,,http://isni.org/isni/0000000088808274,367 +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,11,11 Yleinen ryhmä,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,12,"12 Communication, Communities and Languages",, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,13,13 Opiskelijoiden ohjaus ja kasvu,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,14,14 Pedagogical and community development,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,21,21 Yleinen ryhmä,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,22,22 Aikuis- ja työikäisen väestön kanssa tehtävä työ,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,23,Alusta DIGI-USER,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,24,24 Hoitotyön ja terveyden edistäminen,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,26,"26 Lapsi-, nuoriso- ja perhetyö",, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,31,31 Yleinen ryhmä,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,32,32 Diakonia ja kristillinen kasvatus,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,34,34 Tulkkaus ja moninaisuustyö,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,41,41 Yleinen ryhmä,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,42,42 TKI-opinnot ja -toiminta,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,43,"43 Ylemmät tutkinnot, työkäytännöt ja johtamistoiminta",, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,51,51 Yleinen ryhmä,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,61,61 Rehtoraatti,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,62,"62 Viestintä, tiedotus ja markkinointi",, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,63,63 Henkilöstöpalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,64,64 Talouspalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,65,65 Tietohallintopalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,66,66 Kirjasto ja tietopalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,68,68 Tilapalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,74,"74 Opintotoimistopalvelut, hakutoimisto",, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,75,75 Kansainvälisyyspalvelut,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,,,http://isni.org/isni/0000000406476405,570 +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,AOKK,Ammatillinen opettajankoulutus,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,JOURA,Journalismin ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,ASSI,Johdon assistenttityön ja kielten ko.,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,FINA,Finanssi- ja talousasiantuntijan koulutusohjelma,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,GLOBBA,DP in International Business,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HELI,Liiketalouden ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,LIIPO,Liiketalouden ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,MUBBA,DP for Multilingual Management Assistants,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,MYYNTI,Myyntityön ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,POBBA,DP in International Business,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,SAMPO,DP in International Sales and Marketing,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,VIMA,Myyntityön ja visuaalisen markkinoinnin ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,ABBA,DP in Aviation Business,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HETI,Tietojenkäsittelyn ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,BITE,DP in Business Information Technology,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,LOT,Liikunnan ja vapaa-ajan ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,SPORT,DP in Sports and Leisure Management,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HOTRA,Hotelli- ja ravintola-alan liikkeenjohdon ko.,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,RUOKA,Hotelli- ja ravintola-alan ko.,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,RUOKAT,Ruokatuotannon johtamisen ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,MATKA,Matkailun liikkeenjohdon ko.,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HOSBA,"DP in Hotel, Restaurant and Tourism Management",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HOTEM,"DP in Hospitality, Tourism and Experience Management",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,TOBBA,DP in Tourism,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,POMO,Matkailun ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HTP,Hallinto- ja tukipalvelut,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,,,http://isni.org/isni/0000000404102071,11 +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H01,Yliopistopalvelut,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H10,Teologinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H20,Oikeustieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H30,Lääketieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H40,Humanistinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H50,Matemaattis- luonnontieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H55,Farmasian tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H57,Bio- ja ympäristötieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H60,Käyttäytymistieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H70,Valtiotieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H74,Svenska social- och kommunalhögskolan,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H80,Maatalous- metsätieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H90,Eläinlääketieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H903,Tila- ja kiinteistökeskus,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H906,Kielikeskus,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H907,Tietotekniikkakeskus,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H909,UniSport,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H916,Neurotieteen tutkimuskeskus,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H918,Tutkijakollegium,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H919,Biotekniikan instituutti,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H92,Helsingin yliopiston tutkijakoulut,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H930,Avoin yliopisto,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H945,Suomen molekyylilääketieteen instituutti,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H955,Helsingin yliopiston koe-eläinkeskus,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H960,Helsingin yliopiston koulutus- ja kehittämispalvelut,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H97,Helsinki Institute of Life Science,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H978,Luonnontieteellinen keskusmuseo,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H981,Kansalliskirjasto,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H985,Helsingin yliopiston kirjasto,, +Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,,,http://isni.org/isni/000000040624783X,1875 +Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,10,Taiteet ja kulttuurialat,, +Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,20,Terveys- ja hyvinvointialat,, +Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,30,Humanistiset alat,, +Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,50,Yhteiset,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,,,http://isni.org/isni/0000000106852595,9 +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,1,Yhteiset palvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,11,Yrittäjyys ja liiketoimintaosaaminen,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,4,Ammatillinen opettajakorkeakoulu,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,6,Hyvinvointiosaaminen,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,7,Teknologiaosaaminen,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,8,Biotalous,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,,,http://isni.org/isni/0000000121143658,SXTDpC1TdW9tZW4geWxpb3Bpc3Rv +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,202010,Kasvatustieteiden ja psykologian osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,100000,100000,"Yliopiston johto, yhteiset ja yliopistopalvelut",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,200010,Filosofisen tiedekunnan hallinto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,201010,Humanistinen osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,201020,Humanistinen osasto / Suomen kieli ja kulttuuritieteet,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,201030,Humanistinen osasto / Vieraat kielet ja käännöstiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,202020,Kasvatustieteiden ja psykologian osasto / Erityispedagogiikka,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,202030,"Kasvatustieteiden ja psykologian osasto / Kasvatustiede, aikuiskasvatus ja ohjaus",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,202040,Kasvatustieteiden ja psykologian osasto / Psykologia,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,203010,Soveltavan kasvatustieteen ja opettajankoulutuksen osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,203020,Soveltavan kasvatustieteen ja opettajankoulutuksen osasto / Joensuu,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,203030,Soveltavan kasvatustieteen ja opettajankoulutuksen osasto / Savonlinna,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,204010,Teologian osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,204020,Teologian osasto / Läntinen teologia,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,204030,Teologian osasto / Ortodoksinen teologia,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,205010,Harjoittelukoulut / Joensuu,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,205020,Harjoittelukoulut / Savonlinna,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,300010,Luonnontieteiden ja metsätieteiden tiedekunnan hallinto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,303010,Fysiikan ja matematiikan laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,303510,Sovelletun fysiikan laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,304010,Kemian laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,305010,Metsätieteiden osasto,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,306020,Tietojenkäsittelytieteen laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,307010,Ympäristö- ja biotieteiden laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,308010,SIB-labs -infrastruktuuriyksikkö,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,309010,Mekrijärven tutkimusasema,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,400010,Terveystieteiden tiedekunnan hallinto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,401010,A.I. Virtanen -instituutti / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,401020,A.I. Virtanen -instituutti / Bioteknologia ja molekulaarinen lääketiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,401030,A.I. Virtanen -instituutti / Neurobiologia,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,402010,Farmasian laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,403010,Hoitotieteen laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404010,Lääketieteen laitos / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404020,Lääketieteen laitos / Biolääketiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404030,Lääketieteen laitos / Kansanterveystiede ja kliininen ravitsemustiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404040,Lääketieteen laitos / Kliininen lääketiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404050,Lääketieteen laitos / Hammaslääketiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,405010,Koe-eläinkeskus,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,500010,Yhteiskuntatieteiden ja kauppatieteiden tiedekunnan hallinto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,501010,Historia- ja maantieteiden laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,502010,Kauppatieteiden laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,503010,Oikeustieteiden laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,504010,Sosiaali- ja terveysjohtamisen laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,505010,Yhteiskuntatieteiden laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,506010,Karjalan tutkimuslaitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,507010,Matkailualan opetus- ja tutkimuslaitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,600000,600000,Itä-Suomen yliopiston apteekki,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,700000,700000,Koulutus- ja kehittämispalvelu Aducate,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,800000,800010,Kielikeskus,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,800000,800020,Kirjasto,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,,,http://isni.org/isni/0000000404148475,Snl2w6Rza3lsw6RuIGFtbWF0dGlrb3JrZWFrb3VsdQ== +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,1,Ammatillinen opettajakorkeakoulu,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,2,Hyvinvointiyksikkö,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,3,Liiketoimintayksikkö,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,4,Teknologiayksikkö,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,5,Hallintoyksikkö,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,,,http://isni.org/isni/0000000121799569,Snl2w6Rza3lsw6RuIHlsaW9waXN0bw== +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,20019100,Agora Center,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219110,Avoin yliopisto,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219200,Kirjasto,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219120,Kokkolan yliopistokeskus Chydenius,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,20019140,Koulutuksen tutkimuslaitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219230,Tiedemuseo,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219220,Yliopiston kielikeskus,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219700,Johto ja yliopiston yhteiset,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219500,Yliopistopalvelut,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,213000,Humanistinen tiedekunta,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213050,Historian ja etnologian laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213040,Kielten laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213010,Musiikin laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213060,Soveltavan kielentutkimuksen keskus,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213030,Taiteiden ja kulttuurin tutkimuksen laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213020,Viestintätieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,217000,Informaatioteknologian tiedekunta,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,217000,217010,Tietojenkäsittelytieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,217000,217020,Tietotekniikan laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,216000,Kauppakorkeakoulu,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,216000,216010,Avance Johtamiskoulutus,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,211000,Kasvatustieteiden tiedekunta,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,211000,211010,Opettajankoulutuslaitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,211000,211020,Kasvatustieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,215000,Liikuntatieteellinen tiedekunta,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,215000,215020,Liikuntabiologian laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,215000,215030,Liikuntakasvatuksen laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,215000,215010,Terveystieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,214000,Matemaattis-luonnontieteellinen tiedekunta,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,214000,214020,Fysiikan laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,214000,214030,Kemian laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,214000,214010,Bio- ja ympäristötieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,214000,214040,Matematiikan ja tilastotieteen laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,210100,Normaalikoulu,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,212000,Yhteiskuntatieteellinen tiedekunta,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,212000,212020,Psykologian laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,212000,212010,Yhteiskuntatieteiden ja filosofian laitos,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,,,,http://isni.org/isni/0000000459488864,971 +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,10000,10000,Talous ja kulttuuri yhteiset,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,10000,11000,"Liiketalouden koulutusyksikkö, Kouvola",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,10000,12000,"Liiketalouden koulutusyksikkö, Mikkeli",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,10000,13000,Kulttuurin koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,20000,Tekniikka yhteiset,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,21000,Rakennus- ja energiatekniikan koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,22000,Informaatioteknologian koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,23000,"Sähkö-, talo- ja materiaalitekniikan koulutusyksikkö",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,24000,Metsätalouden ja ympäristöteknologian koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,25000,Logistiikan ja merenkulun koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,30000,Hyvinvointi yhteiset,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,31000,Kuntoutus- ja terveysalan koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,32000,Terveysalan ja ensihoidon koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,33000,Sosiaali- ja terveysalan koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,34000,Sosiaalialan ja toimintakyvyn edistämisen koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,35000,"Matkailu-,ravitsemis- ja nuorisoalan koulutusyksikkö",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,,40000,Opetuksen hallinto,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,,51000,Opetuksen palvelut,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,,70000,"Talous, henkilöstö- ja hallintopalvelut",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,60000,TKI ja palvelut,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,61000,TKI-palvelut,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62100,Digitaalinen talous,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62200,"Metsä, ympäristö ja energia",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62300,Logistiikka ja merenkulku,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62400,Kestävä hyvinvointi,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62500,Luovat alat,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66000,Kuitulaboratorio,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66600,Kymilabs,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66100,3K-tehdas,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66200,Nuorisoalan tutkimus- ja kehittämiskeskus Juvenia,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66300,Mikpolis,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66900,Pienyrityskeskus,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,,,http://isni.org/isni/0000000404149419,993 +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,10,Liiketoiminta ja innovaatiot,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,11,Kone- ja kaivostekniikka,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,12,Tietojärjestelmät,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,13,Sairaan- ja terveydenhoito,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,14,Aktiviteettimatkailu,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,15,Aikuis- ja täydennyskoulutuspalvelut AIKOPA,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,16,Koulutuspalvelut,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,17,TKI-palvelut,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,18,Kampuspalvelut,, +Karelia,Karelia University of Applied Sciences,,02469,,,,http://isni.org/isni/0000000406476472,1013 +Karelia,Karelia University of Applied Sciences,,02469,,1054,1054 Rehtorin toimisto,, +Karelia,Karelia University of Applied Sciences,,02469,,1055,1055 Opintoasiainpalvelut,, +Karelia,Karelia University of Applied Sciences,,02469,,1057,1057 Laskentapalvelut,, +Karelia,Karelia University of Applied Sciences,,02469,,1060,1060 Henkilöstöpalvelut,, +Karelia,Karelia University of Applied Sciences,,02469,,1063,1063 Tiedotus- ja markkinointi,, +Karelia,Karelia University of Applied Sciences,,02469,,1066,1066 Tietohallinto,, +Karelia,Karelia University of Applied Sciences,,02469,,1069,1069 Kirjasto,, +Karelia,Karelia University of Applied Sciences,,02469,,1072,1072 Kansainvälistymispalvelut,, +Karelia,Karelia University of Applied Sciences,,02469,,1970,1970 Projektitoiminta/YPA,, +Karelia,Karelia University of Applied Sciences,,02469,,2051,2051 TKI- ja palveluliiketoiminta,, +Karelia,Karelia University of Applied Sciences,,02469,,3051,3051 Metsätalous,, +Karelia,Karelia University of Applied Sciences,,02469,,3054,3054 Maaseutuelinkeinot,, +Karelia,Karelia University of Applied Sciences,,02469,,3101,3101 Energia- ja ympäristötekniikka,, +Karelia,Karelia University of Applied Sciences,,02469,,5000,5000 Koulutuksen hallinto/Wärtsilä,, +Karelia,Karelia University of Applied Sciences,,02469,,5011,5011 TKI/Projektit/Tekniikan alat,, +Karelia,Karelia University of Applied Sciences,,02469,,5031,5031 Tietojenkäsittely,, +Karelia,Karelia University of Applied Sciences,,02469,,5051,5051 Konetekniikka,, +Karelia,Karelia University of Applied Sciences,,02469,,5054,5054 Rakennustekniikka,, +Karelia,Karelia University of Applied Sciences,,02469,,5057,5057 Tieto- ja viestintätekniikka,, +Karelia,Karelia University of Applied Sciences,,02469,,5061,5061 Talotekniikka,, +Karelia,Karelia University of Applied Sciences,,02469,,5064,5064 Sähkötekniikka,, +Karelia,Karelia University of Applied Sciences,,02469,,5067,5067 YAMK Teknologiaosaamisen johtaminen,, +Karelia,Karelia University of Applied Sciences,,02469,,5101,5101 Liiketalous,, +Karelia,Karelia University of Applied Sciences,,02469,,5104,5104 International business,, +Karelia,Karelia University of Applied Sciences,,02469,,5110,5110 YAMK Johtamisen ja liiketoimintaosaamisen koulutus,, +Karelia,Karelia University of Applied Sciences,,02469,,5999,5999 Elinkeinotoiminta/tekniikka,, +Karelia,Karelia University of Applied Sciences,,02469,,6051,6051 Medianomikoulutus,, +Karelia,Karelia University of Applied Sciences,,02469,,6054,6054 Musiikkipedagogikoulutus,, +Karelia,Karelia University of Applied Sciences,,02469,,6057,6057 Muotoilu,, +Karelia,Karelia University of Applied Sciences,,02469,,6151,6151 Restonomi,, +Karelia,Karelia University of Applied Sciences,,02469,,7000,7000 Koulutuksen hallinto/Tikkarinne,, +Karelia,Karelia University of Applied Sciences,,02469,,7051,7051 Sosionomi,, +Karelia,Karelia University of Applied Sciences,,02469,,7054,7054 Sairaanhoitaja,, +Karelia,Karelia University of Applied Sciences,,02469,,7057,7057 Fysioterapeutti,, +Karelia,Karelia University of Applied Sciences,,02469,,7061,7061 Terveydenhoitaja,, +Karelia,Karelia University of Applied Sciences,,02469,,7063,7063 YAMK Sosiaali- ja terveysalan kehittäminen ja johtaminen,, +Karelia,Karelia University of Applied Sciences,,02469,,7064,7064 YAMK Ikäosaaminen,, +Karelia,Karelia University of Applied Sciences,,02469,,8067,8067 Maakuntakorkeakoulu,, +Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,,,http://isni.org/isni/0000000404178973,1109 +Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,LI,Liiketalous ja matkailu,, +Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,LP,LAMK Palvelut,, +Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,MD,Musiikki- ja draamainstituutti,, +Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,MI,Muotoiluinstituutti,, +Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,ST,Sosiaali- ja terveys,, +Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,TE,Tekniikka ja ympäristö,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,,,http://isni.org/isni/0000000446491976,1133 +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,120,Hyvinvointipalvelut,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,130,Kauppa ja kulttuuri,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,140,Matkailupalvelut,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,150,Teollisuus ja luonnonvarat,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,160,YAMK,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,110,Yhteiset toiminnot,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,111,Korkeakoulupalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,,,http://isni.org/isni/000000010744995X,3448 +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,221200,Harjoittelukoulu,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,222000,Oikeustieteiden tiedekunta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,225000,Arktinen keskus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250600,Arktisen antropologian tutkimusryhmä,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226100,Avoin yliopisto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,20026300,200263000,Kirjaston hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,20026300,200263050,Konsernin kirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,20026300,2263200,Taidekirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,20026300,2263400,AK kirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226350,Lapin yliopistokustannus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226400,Kielikeskus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2293300,Lapin korkeakoulukonserni,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,229800,Yliopistotason tulot ja menot,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,221000,Kasvatustieteiden tiedekunta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,223000,Taiteiden tiedekunta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,221000,2210000,KTK hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,221000,2211100,KTK perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,221000,2211200,Opetus- ja kasvatusalan koulutuspalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,221200,2212000,Harjoittelukoulun hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,221200,2212100,Harjoittelukoulu perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,222000,2220000,OTK hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,222000,2220100,OTK perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,223000,2230000,TTK hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,223000,2230100,TTK perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,223000,2230110,TTK elinkeinotoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,223000,2230120,TTK täydentävä rahoitus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,224000,Yhteiskuntatieteiden tiedekunta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224000,2240000,YTK hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224000,2240100,YTK perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,224100,Pohjoisen yhteiskunnan tutkimusinstituutti,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224100,2241100,Pohjoisen yhteiskunnan tutkimusinstituutti,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,224500,Matkailun tutkimus- ja koulutusinstituutti,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224500,2245000,MTI hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224500,2245100,Matkailun tutkimus- ja koulutusinstituutti,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224500,2245200,MTI täydentävä rahoitus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250000,Arktinen hallinta ja hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250100,Tiedekeskusnäyttely,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250200,Tiedeviestintä,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250300,Pohjoisen ympäristö- ja vähemmistöoikeuden instituutti,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,200250400,Arktisen kestävän kehityksen tutkimusryhmä,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250500,Arktisen globaalimuutoksen tutkimusryhmä,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226100,2261100,Avoin yliopisto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226200,Koulutus- ja kehittämispalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226200,2262100,Koulutus- ja kehittämispalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,20026300,Pääkirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,20026300,200263100,Pääkirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226350,2263500,Lapin yliopistokustannus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226400,2264000,Kielikeskuksen hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226400,2264100,Kielikeskus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226400,2264200,Käännöspalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226400,2264300,Konsernin kielikeskus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,229000,Hallintoyksikkö,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290100,Rehtori,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290200,Yleishallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290300,Suunnittelupalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290310,Taloushallintopalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290500,Viestintä- ja suhdetoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290600,Täydentävän rahoituksen palvelupiste,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290700,Tekniset erityispalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290800,Laadunhallintapalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291100,Vararehtori,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291200,Henkilöstöasiat,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291300,Tietohallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291400,IT-palvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291500,Tilahallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291600,Hankintatoimi,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291700,Yliopistopaino,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291800,Tutkimuspalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2292100,Opetusvararehtori,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2292200,Opiskelijapalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2293100,Tiedevararehtori,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2294100,Kongressipalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2295100,University of Arctic -rahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2296100,Lapin yliopistorahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2297100,Kansainväliset asiat,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2297500,Uarctic-verkosto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298400,Henkilöstöasiat,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298801,Kulttuurilähtöinen palvelumuotoilu -tohtoriohjelma,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298802,Pohjoiset kulttuurit ja kestävä luonnonvarapolitiikka,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298803,Yhteisöt ja muuttuva työ,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298804,Tieteenfilosofia ja tutkijantaidot,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299010,Kasvatustieteiden opetuksen ja tutkimuksen rahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299020,Oikeustieteiden opetuksen ja tutkimuksen rahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299030,Taiteiden opetuksen ja tutkimuksen rahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299040,Yhteiskuntatieteiden opetuksen ja tutkimuksen rahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299050,Arktisen opetuksen ja tutkimuksen rahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299100,Lapin yliopiston profilointirahasto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,,,http://isni.org/isni/0000000105333048,1150 +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A100A,PÄÄT Tiedekunnan yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A110A,PÄÄT Kanslia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A120A,PÄÄT Tiedekunnan opintopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A130A,PÄÄT KTK-yleisopinnot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A200A,PÄÄT Talouden ja yritysjuridiikan laitos,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A210A,PÄÄT Laskentatoimi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A220A,PÄÄT Rahoitus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A230A,PÄÄT Strategiatutkimus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A240A,PÄÄT Yritysjuridiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A250A,PÄÄT Talousjohtaminen (KTK),, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A300A,PÄÄT Johtamisen ja kv-liiketoiminnan lai,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A310A,PÄÄT Hankintojen johtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A320A,PÄÄT Johtaminen ja organisaatiot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A330A,PÄÄT Kansainvälinen markkinointi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A340A,PÄÄT Tietojohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A350A,PÄÄT MSIS,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A360E,PÄÄTLahden yksikkö,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A365A,PÄÄT Tietojohtaminen ja johtajuus (KTM),, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A370A,PÄÄT Johtaminen (KTK),, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A380A,PÄÄT Kv-liiketoiminta (KTK),, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A390A,PÄÄT Strateginen johtaminen (KTM),, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A410A,PÄÄT Nordi / KATI,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A500A,PÄÄT TBRC/KATI,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B100A,Teknillisen tiedekunnan yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B101A,PÄÄT Hybridilaboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B102A,"Yk-lisät,Tekn.tdk yhteiset",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B110A,Toimistopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B111A,PÄÄT Toimistopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B120A,Opintopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B130A,PÄÄT Tekniset palvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B131A,PÄÄT Elektroniikan suunnittelukeskus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B132A,PÄÄT Mekaniikkapaja,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B133A,PÄÄT TEPA Energia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B134A,PÄÄT TEPA Kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B135A,PÄÄT TEPA Metalli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B140A,Kielikeskus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B141A,PÄÄT Kielikeskus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B150A,PÄÄT CST,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B170A,PÄÄT LUT voima,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B200A,LUT School of Energy Systems yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B201A,"Yk-lisät, LES",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B210A,Energiatekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B211A,Lämpö- ja virtaustekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B212A,Teknillinen termodynamiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B213A,Virtaustekniikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B214A,Uusiutuvien energiajärjestelmien laborat,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B215A,PÄÄT Ydinvoimatekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B216A,PÄÄT Ydinturvallisuuden tutkimusyksikkö,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B217A,Ydintekniikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B220G,Bioenergian laboratorio/Mikkeli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B221J,PÄÄT Energia/Varkaus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B230A,Sähkötekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B231A,Sähkömarkkinalaboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B232A,Sähkönkäyttötekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B233A,CDMC-laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B234A,Säätö- ja digitaalitekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B235A,Sovelletun elektroniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B236A,PÄÄT Sähkömekaaniset järjestelmät,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B237A,Aurinkotalousprofessuuri,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B240A,"Kestävyystutkimus, Lappeenranta",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B241E,"Kestävyystutkimus, Lahti",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B241G,PÄÄT Vihreä kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B250A,LUT Kone yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B250G,PÄÄT Materiaalitekniikka/Mikkeli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B251A,Hitsaustekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B252N,Koneteknologia/Turku,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B253A,Lasertyöstö,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B254A,Tuotantotekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B255A,Koneensuunnittelun laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B256A,Teräsrakenteiden laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B257C,Kuitukomposiittilaboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B258A,Konedynamiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B259A,Älykkäiden koneiden laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B260G,PÄÄT LUT Savo,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B290A,LUT Voima,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2AAA,Alusta LES Reflex,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2ABA,Alusta LES SIM,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2ACA,Alusta LES RE-SOURSE,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2ADA,Alusta LES DIGI-USER,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2AEA,Alusta LES SAWE,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2AFA,Alusta LES RED,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B300A,LUT Kemia yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B301A,CST,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B302A,Analyysipalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B303A,LENS tekniset palvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B304A,Matematiikan ja fysiikan laitoksen yht,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B305A,"Opetus, laskennallinen tekniikka",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B306A,"Opetus, kemian tekniikka",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B308A,"Yk-lisät, LENS",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B309A,School of Engineering Science yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B310A,PÄÄT Kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B311A,PÄÄT Teknillinen kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B312A,PÄÄT Membraanitekniikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B320A,PÄÄT Yksikköoperaatiot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B321A,PÄÄT Tuote- ja prosessikehitys,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B322A,PÄÄT Systeemitekniikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B323A,PÄÄT Fysikaalinen kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B324A,PÄÄT Kiinteä/neste-erotus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B325A,PÄÄT Virtausprosessi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B326A,PÄÄT Erotustekniikka ja prosessi-integro,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B330A,PÄÄT Kuitu- ja paperitekniikan laborator,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B331A,PÄÄT Paperinjalostus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B340H,PÄÄT Fiber Tech -tutkimusyksikkö/Savonli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B351A,Kemialliset erotustekniikat,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B352A,Membraanitekniikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B353A,Termiset yksikköoperaatiot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B354A,Kiinteä/neste-erotus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B361A,Prosessikehitys,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B362A,Virtausprosessit,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B363A,Erotuskonseptit,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B364A,PÄÄT Biomateriaalit,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B365A,Kemiallinen metrologia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B371G,Vihreä kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B380A,Inversio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B381A,Laskennallinen materiaalitiede,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B382A,Kiinteän aineen fysiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B383H,Materiaalifysiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B385A,Optoelektroniikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B386A,Konenäkö ja hahmontunnistus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B387D,PÄÄT Konenäkö ja hahmotunnistus Kouvola,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B388A,PÄÄT Kemiallinen metrologia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B390A,CEID-keskus yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B391J,Varkaus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3AAA,Alusta LENS Reflex,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3ABA,Alusta LENS SIM,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3ACA,Alusta LENS RE-SOURSE,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3ADA,Alusta LENS DIGI-USER,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3AEA,Alusta LENS SAWE,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3AFA,Alusta LENS RED,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B400A,PÄÄT LUT Kone yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B410A,PÄÄT Hitsaustekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B411N,PÄÄT Koneteknologia/Turku,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B412A,PÄÄT Lasertyöstö,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B413G,PÄÄT Materiaalitekniikka/Mikkeli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B420A,PÄÄT Konepajan ja levytyötekniikan labor,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B430A,PÄÄT Koneensuunnittelun laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B440A,PÄÄT Teräsrakenteiden laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B450C,PÄÄT Kuitukomposiittilaboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B460A,PÄÄT Konedynamiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B470A,PÄÄT Älykkäiden koneiden laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B500A,PÄÄT Matematiikan ja fysiikan laitokset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B505A,PÄÄT Mafy yhteiset opetus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B510A,PÄÄT Inversio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B511G,PÄÄT Materiaalitekniikka/Mikkeli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B515A,PÄÄT Laskennallinen materiaalitiede,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B520A,PÄÄT Materiaalifysiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B525A,PÄÄT Optoelektroniikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B530A,PÄÄT Konenäkö ja hahmotunnistus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B531D,PÄÄT Konenäkö ja hahmontunnistus Kouvola,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B600A,PÄÄT CEID-keskus yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B610A,PÄÄT Älykkäiden koneiden laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B620J,PÄÄT Varkaus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C100A,PÄÄT Tiedekunnan hallintopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C110A,PÄÄT Tiedekunnan opintopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C120A,PÄÄT Tiedekunnan yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C200A,PÄÄT Tuotantotalouden osaston yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C210A,PÄÄT Teollisen markkinoinnin ja kansainv,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C211A,PÄÄT Kansainväliset toiminnot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C212A,PÄÄT Teollinen markkinointi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C220A,PÄÄT Toimitusketjun johtamisen laborator,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C221A,PÄÄT Toimitusketjun johtaminen/TBRC,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C230A,PÄÄT Teollisuustalouden laboratorio yhte,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C231A,PÄÄT Teollisuustalous,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C232A,PÄÄT Päätöksenteontukisysteemit,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C233A,PÄÄT Kustannusjohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C234A,PÄÄT Teknologiayrittäjyys,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C235A,PÄÄT Tietojohtaminen/tuotantotalous,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C236A,PÄÄT Innovaatio- ja teknologiajohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C240A,PÄÄT Ryhmäpäätöksenteon ja teknologian j,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C241M,PÄÄT Tuotantotalous/Lapua,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C250E,PÄÄT Lahti/School of Innovation,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C260D,PÄÄT Tuotantotalous/Kouvola yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C261D,PÄÄT Tuotantotalous/Kouvola,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C262D,PÄÄT Rautatielogistiikka/Kouvola,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C270A,PÄÄT Innovaatio- ja teknologiajohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C271A,PÄÄT Innovaatio- ja teknologiajohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C272A,PÄÄT Innovaatio- ja teknolog./TBRC,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C281A,PÄÄT Tieto- ja kustannusjohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C283A,PÄÄT Tieto- ja kustannusjohtaminen/TBRC,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C300A,PÄÄT Ohjelmistotuotanto ja tiedonhallint,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C310A,PÄÄT Tietoliikenneohjelmistojen laborato,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C320A,PÄÄT Konenäön ja hahmontunnistuksen labo,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C321D,PÄÄT Tietoyhteiskunnan teknologiat/Kouvo,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C330A,PÄÄT Ohjelmistotekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C340A,PÄÄT Tietojohtaminen/tuta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C400A,PÄÄT Innovaatiojohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C500A,PÄÄT Arvoverkostojen johtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C600D,PÄÄT Kouvolan yksikkö,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C700E,PÄÄT Lahti School of Innovation,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C800A,PÄÄT Ohjelmistotutanto ja tiedonhallinta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E100A,Kanslia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E101A,Opintopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E102A,Schoolin yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E103A,"Yk-lisät, LBM",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E110A,"Strategy,management and accounting",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E120A,"International business,marketing,entrepr",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E121G,IBME/Mikkeli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E130A,Innovation and software,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E140A,Operations management and systems engine,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E150E,"Internat business,marketing,entrep/LAHTI",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E151E,Innovation and software/LAHTI,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E152E,Operations management and systems/LAHTI,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E153E,Lahti/common,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E160D,Innovation and software/KOUVOLA,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E161D,Operations management and system/KOUVOLA,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E162D,Kouvola/common,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E163D,Kouvola,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E170A,Etelä-Karjala-instituutti,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E180A,IEM Yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E181A,Software Engineering,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E182A,Innovation Management,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E183A,Cost Management,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E184A,Systems Engineering,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E185A,Supply Chain Management,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1AAA,Alusta LBM Reflex,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1ABA,Alusta LBM SIM,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1ACA,Alusta LBM RE-SOURSE,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1ADA,Alusta LBM DIGI-USER,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1AEA,Alusta LBM SAWE,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1AFA,Alusta LBM RED,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F100A,Koke yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F101A,"Yk-lisät, Koulutus- ja kehittämiskeskus",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F110A,Täydennyskoulutus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F120A,Tutkintotavoitteinen koulutus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F130A,Avoin yliopisto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F140A,Tutkimus- ja kehittämistoiminta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F150A,Asiakkuudet,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F200A,Kirjasto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F201A,"Yk-lisät, Kirjasto",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F210A,Yhteinen tiedekirjasto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F220A,Imatran toimipiste,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F230A,Saimia Kirjasto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F300A,PÄÄT Etelä-Karjala-instituutti,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F400A,PÄÄT Nordi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23G300A,PÄÄT TBRC,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J100A,Yliopiston johto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J110A,Toiminta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J120A,PÄÄT Strategia ja yhteiskuntasuhteet,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J130A,Lakiasiat,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J200A,Toiminnan ohjaus ja laadunhallinta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J210A,Toiminnan suunnittelu,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J300A,Erityishankkeet,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J310A,LUMA,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J400A,LUT Investointiohjelma,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J410A,IPR ja patentit,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J500A,Alusta REFLEX,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J501A,Alusta SIM,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J502A,Aloittavat tutkimusalustat,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J503A,Alusta RE-SOURSE,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J504A,Alusta DIGI-USER,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J505A,Alusta SAWE,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J506A,Alusta RED,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K100A,Opintotoimisto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K110A,Hakupalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K120A,Opintoasioiden kehittäminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K200A,Talouspalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K210A,Kirjanpito,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K220A,Sisäinen laskenta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K300A,Tutkimus- ja innovaatiopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K301A,Nordi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K302A,TBRC,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K310A,Kansallinen rahoitus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K320A,EU-rahoitus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K330A,Kumppanuudet ja Venäjätoiminnot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K340A,Tutkimuspolitiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K400A,IS&T,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K410A,PÄÄT Käyttötuki,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K420A,PÄÄT Project Office,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K430A,PÄÄT Palvelutuotanto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K440A,PÄÄT AV-palvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K450A,Palvelukehitys,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K460A,Perusresurssipalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K461A,Tulostus- ja kopiointipalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K462A,AV-palvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K470A,Käyttötukipalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K471A,Hankintapalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K472A,Mikroluokkapalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K473A,Puhelinpalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K480A,Opetusteknologia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K490A,PÄÄT Digipaino,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K500A,HR,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K600A,Tilapalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K610A,Talouspalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K620A,Kirjanpito,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K630A,Sisäinen laskenta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K640A,Rahoitus ja tarkastustoiminta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K680A,Tilapalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K690A,Tilakustannukset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K700A,Viestintäpalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K800A,Kansainvälisyys,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K810A,"Apurahajärjestelmä, LUT",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K820A,Opiskelijapalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K830A,Koulutuspalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K840A,Tietojärjestelmien kehittäminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K850A,Apurahat,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K860A,Kielikeskus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K870A,Opetusteknologia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K900A,Yliopistopaino,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KA00A,LUT tohtorikoulu,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB10A,Toiminnan ohjaus ja laadun hallinta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB11A,Toiminnan suunnittelu,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB20A,IS&T + Project office,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB21A,Front Office,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB22A,Back Office,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB23A,Yliopistopaino,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB40A,Lähipalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB50A,Yhteiskirjasto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB51A,LUTin kirjastopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB52A,Saimian kirjastopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB53A,Imatran toimipiste,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB54A,"Yk-lisät, Kirjasto",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC10A,"Innovaatiot ja yrittäjyys, yhteiset",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC11A,"Innovaatiot ja yrittäjyys, yk-laskenta",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC20A,Yrittäjyys,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC30A,Innovaatiopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC40A,Täydennyskoulutus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC50A,Urapalvelut,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,,,http://isni.org/isni/0000000404001203,1179 +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,HL,Liiketalous,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,HS,Turvallisuusala,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,LAU,Laurea,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,MP,Matkailu- ja palveluliiketoiminta,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,NT,Tietojenkäsittely,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,SF,Fysioterapia,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,SH,Terveysala,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,SK,Kauneudenhoitoala,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,SS,Sosiaaliala,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,ST,Sosiaali- ja terveysala,, +Maanpuolustuskorkeakoulu, The National Defence University,Försvarshögskolan,02358,,,,http://isni.org/isni/0000000406476253,1232 +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,,,http://isni.org/isni/0000000119134955,24 +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,180,Rehtori,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,189,Talous- ja hallintopalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,190,Henkilöstöpalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,191,Tietohallintopalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,207,Kirjasto- ja tietopalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,208,Kv-tukipalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,209,Opintoasiainpalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,219,Järjestelmäylläpito,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,220,Käyttäjätuki,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,221,Tiedonhallinta- ja järjestelmäpalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,267,Electria,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,293,Taloussuunnittelu- ja seurantapalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,294,Kirjanpito- ja maksuliikennepalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,315,Kiinteistöpalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,319,Koulutuksen kehittämispalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,329,Hakijapalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,333,Opintotoimistot,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,340,Viestintäyksikkö,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,341,"Kirjasto- ja tietopalvelut, TeKu-tiimi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,343,"Kirjasto- ja tietopalvelut, Sote-tiimi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,344,"Kirjasto- ja tietopalvelut, Lite-tiimi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,357,Opiskeluhyvinvointitoiminta,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,358,Korkeakoulupalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,400,Liiketoimintapalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,402,Strategia- ja kehityspalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,403,Oppimistoiminta,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,404,TKI-toiminta,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,405,Liiketoiminta,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,406,Tuotantotalous,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,407,Rakentaminen ja arkkitehtuuri,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,408,Puhtaat teknologiat,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,409,Liikkuminen ja toimintakyky,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,410,Kiinteistö- ja talotekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,411,Kliinisen hoitotyön ja ensihoidon palvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,412,Osallistuminen ja toimintakyky,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,413,Terveyden edistämisen palvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,414,Musiikki,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,415,Tieto- ja viestintätekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,416,Kulttuuripalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,417,Sähkö- ja automaatiotekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,418,Kansainvälinen liiketoiminta,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,419,Media,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,420,Terveysalan tutkimuspalvelut ja palvelujohtaminen,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,421,Ajoneuvo- ja konetekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,422,Sosiaalinen hyvinvointi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,423,Esittävä taide,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,424,Konservointi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,425,Kulttuurituotanto,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,426,Muotoilu,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,427,Vaatetus,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,428,"Kv-liiketoiminta, hallinto",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,429,"Kv-liiketoiminta, YAMK ja TKI",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,430,Sähkövoimatekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,431,Automaatiotekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,432,"Sähkö- ja automaatiotekniikka, TKi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,433,Elektroniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,434,Tietoverkkotekniikan tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,435,Mediatekniikan tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,436,"Informaatiotekniikka ja pelisovellukset, tiimi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,437,Kehitysjohtajan yksikkö,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,438,Näyttökeskus,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,,,http://isni.org/isni/0000000094586751,1473 +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10200,Hallintoyksikkö,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10210,Kehittäminen,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10211,Viestintä,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10212,Hallintopalvelut,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10220,Ammatillinen opettajakoulutus,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10230,Hoitoalat (Oulu ja Oulainen),, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10235,Terveydenhuollon erityisalat,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10240,Sosiaaliala ja kuntoutus,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10250,Liiketalous,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10260,Energia ja automaatio,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10265,Rakentamistekniikka,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10270,Konetekniikka,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10275,Informaatioteknologia,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10280,Luonnonvara-ala,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10290,Kulttuuri,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,,,http://isni.org/isni/0000000404505858,1478 +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24000,HUMANISTINEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240000,Humanistinen tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240000,2400000,Humanistinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240000,2400001,HuTK koulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240030,Aate- ja oppihistoria,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240030,2400300,Aate- ja oppihistoria,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240031,Englantilainen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240031,2400310,Englantilainen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240032,Germaaninen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240032,2400320,Germaaninen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240033,Historia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240033,2400330,Historia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240034,Informaatiotutkimus ja viestintä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240034,2400340,Informaatiotutkimus ja viestintä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240034,2400341,Tiedeviestinnän maisteriohjelma,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240035,Kirjallisuus ja elokuvatutkimus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240035,2400350,Kirjallisuus ja elokuvatutkimus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240036,Kulttuuriantropologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240036,2400360,Kulttuuriantropologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240037,Logopedia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240037,2400370,Logopedia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240038,Pohjoismainen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240038,2400380,Pohjoismainen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240039,Suomen kieli,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240039,2400390,Suomen kieli,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240041,Arkeologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240041,2400410,Arkeologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240020,Giellagas-instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240020,2400200,Giellagas-instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24010,KASVATUSTIETEIDEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24010,24011,KASVATUSTIETEIDEN TIEDEKUNTA ilman harjoittelukouluja,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24011,240100,Kasvatustieteiden tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240100,2401000,Kasvatustieteiden tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240100,2401001,KTK koulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24011,240103,Oppiminen ja oppimisprosessit,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240103,2401030,Oppiminen ja oppimisprosessit,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24011,240104,"Opettajat, opettaminen ja kasvatusyhteisöt",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240104,2401040,"Opettajat, opettaminen ja kasvatusyhteisöt",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24011,240105,"Kasvatuksen arvot, aatteet ja yhteiskunnalliset kontekstit",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240105,2401050,"Kasvatuksen arvot, aatteet ja yhteiskunnalliset kontekstit",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24010,24015,Oulun yliopiston harjoittelukoulut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24015,240150,Oulun yliopiston harjoittelukoulu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240150,2401500,Oulun yliopiston harj.koulu yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24015,240151,"Oulun normaalikoulu, Linnanmaa (0-6)",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240151,2401510,"Oulun normaalikoulu, Linnanmaa (0-6)",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24015,240152,"Oulun normaalikoulu, Linnanmaa (7-9)",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240152,2401520,"Oulun normaalikoulu, Linnanmaa (7-9)",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24015,240153,"Oulun normaalikoulu, Koskela 1-6",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240153,2401530,"Oulun normaalikoulu, Koskela 1-6",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24015,240155,"Oulun normaalikoulu, lukio",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240155,2401550,"Oulun normaalikoulu, lukio",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24020,LUONNONTIETEELLINEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240200,Luonnontieteellinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240200,2402000,Luonnontieteellinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240211,Kemikaalipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240211,2402110,Kemikaalipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240206,Maantiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240206,2402060,Maantiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240213,Ekologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240213,2402130,Ekologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240214,Genetiikka ja fysiologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240214,2402140,Genetiikka ja fysiologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240215,Avaruusilmasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240215,2402150,Avaruusilmasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240216,Epäorgaaninen kemia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240216,2402160,Epäorgaaninen kemia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240217,Kestävä kemia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240217,2402170,Kestävä kemia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240207,Matematiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240207,2402070,Matematiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240218,Sovellettu ja laskennallinen matematiikka 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240218,2402180,Sovellettu ja laskennallinen matematiikka 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240219,Sovellettu matematiikka ja tilastotiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240219,2402190,Sovellettu matematiikka ja tilastotiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240220,Neurobiofysiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240220,2402200,Neurobiofysiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240221,Molekylaariset järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240221,2402210,Molekylaariset järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240222,NMR Spectroskopia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240222,2402220,NMR Spectroskopia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240223,Teoreettinen fysiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240223,2402230,Teoreettinen fysiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240224,Tähtitiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240224,2402240,Tähtitiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240225,Ionosfäärifysiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240225,2402250,Ionosfäärifysiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240230,Biologian ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240230,2402300,Biologian ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240231,Fysiikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240231,2402310,Fysiikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240232,Kemian ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240232,2402320,Kemian ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240233,Maantieteen ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240233,2402330,Maantieteen ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240234,Matematiikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240234,2402340,Matematiikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240234,2402341,OuLUMA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24025,KAIVANNAISALAN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24025,240250,Geotieteen tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240250,2402500,Geotieteen tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240250,2402501,KaTK koulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24025,240251,Kaivos- ja rikastustekniikan tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240251,2402510,Kaivos- ja rikastustekniikan tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24030,LÄÄKETIETEELLINEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240300,Lääketieteellinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240300,2403000,Lääketieteellinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240300,2403002,Lääketieteen ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240300,2403003,Hammaslääketieteen ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240300,2403004,Terveystieteiden ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240305,Suun terveyden tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240305,2403050,Suun terveyden tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240309,CERH,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240309,2403090,CERH,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240331,Medical Research Center (MRC),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240331,2403310,Medical Research Center (MRC),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240332,Pohjois-Suomen syntymäkohortti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240332,2403320,Pohjois-Suomen syntymäkohortti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240333,Sisätautien tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240333,2403330,Sisätautien tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240334,ELITE,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240334,2403340,ELITE,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240335,KNK- ja silmätautien tutkimusyksikkö 31.1.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240335,2403350,KNK- ja silmätautien tutkimusyksikkö 31.1.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240336,Neurotieteen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240336,2403360,Neurotieteen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240337,PEDEGO-tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240337,2403370,PEDEGO-tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240338,"Kirurgian, anestesiologian ja tehohoidon tutkimusyksikkö",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240338,2403380,"Kirurgian, anestesiologian ja tehohoidon tutkimusyksikkö",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240339,Hoitotieteen ja terveyshallintotieteen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240339,2403390,Hoitotieteen ja terveyshallintotieteen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240340,"Lääketieteellisen kuvantamisen, fysiikan ja tekniikan tutkimusyksikkö",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240340,2403400,"Lääketieteellisen kuvantamisen, fysiikan ja tekniikan tutkimusyksikkö",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240341,Syövän ja translationaalisen lääketieteen tutkimyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240341,2403410,Syövän ja translationaalisen lääketieteen tutkimyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240342,Biolääketieteellinen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240342,2403420,Biolääketieteellinen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240343,Biopankki Borealis,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240343,2403430,Biopankki Borealis,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24040,OULUN YLIOPISTON KAUPPAKORKEAKOULU,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240400,Oulun yliopiston kauppakorkeakoulu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240400,2404000,Oulun yliopiston kauppakorkeakoulu yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240402,Johtamisen ja kansainvälisen liiketoiminnan yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240402,2404020,Johtamisen ja kansainvälisen liiketoiminnan yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240403,Taloustieteen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240403,2404030,Taloustieteen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240404,Laskentatoimen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240404,2404040,Laskentatoimen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240405,Markkinoinnin yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240405,2404050,Markkinoinnin yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240406,Rahoituksen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240406,2404060,Rahoituksen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240407,Martti Ahtisaari instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240407,2404070,Martti Ahtisaari Instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240408,Executive Education,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240408,2404080,Executive Education,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24050,TEKNILLINEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240500,Teknillinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240500,2405000,Teknillinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240541,Tiedekuntalaboratorio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240541,2405410,Tiedekuntalaboratorio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240542,FabLab TTK,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240542,2405420,FabLab TTK,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240543,Konetekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240543,2405430,Konetekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240544,Prosessi- ja ympäristötekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240544,2405440,Prosessi- ja ympäristötekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240545,Tuotantotalouden ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240545,2405450,Tuotantotalouden ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240546,Tohtorikoulutettavat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240546,2405460,Tohtorikoulutettavat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240547,Opetustilat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240547,2405470,Opetustilat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240548,Energia- ja ympäristötekniikan tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240548,2405480,Energia- ja ympäristötekniikan tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240538,Työpaja,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240538,2405380,Työpaja,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240550,Prosessi- ja ympäristötekniikan osasto (ei käytössä),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240550,2405500,Prosessi- ja ympäristötekniikan os. yht. (ei käytössä),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240550,2405610,Minipilot-rikastamo (ei käytössä),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240532,Mekatroniikka ja konediagnostiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240532,2405320,Mekatroniikka ja konediagnostiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240533,Koneensuunnittelu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240533,2405330,Koneensuunnittelu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240534,Materiaali- ja tuotantotekniikan tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240534,2405340,Materiaali- ja tuotantotekniikan tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240536,Rakenteet ja rakentamisteknologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240536,2405360,Rakennesuunnittelun ja rakentamisteknologian tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240552,Vesi- ja ympäristötekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240552,2405520,Vesi- ja ympäristötekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240554,Kemiallinen prosessitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240554,2405540,Kemiallinen prosessitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240555,Ympäristö- ja kemiantekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240555,2405550,Ympäristö- ja kemiantekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240557,Kuitu- ja partikkelitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240557,2405570,Kuitu- ja partikkelitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240558,Säätötekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240558,2405580,Säätötekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240559,Systeemitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240559,2405590,Systeemitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240560,Prosessimetallurgia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240560,2405600,Prosessimetallurgia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240570,Tuotantotalous,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240570,2405700,Tuotantotalous,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240579,Konepaja- ja metalliosaamisen innovaatiokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240579,2405790,Konepaja- ja metalliosaamisen innovaatiokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24031,BIOKEMIAN JA MOLEKYYLILÄÄKETIETEEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240201,Biokemian ja molekyylilääketieteen tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240201,2402010,Biokemian ja molekyylilääketieteen tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240260,Eklund research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240260,2402600,Eklund research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240261,Hiltunen research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240261,2402610,Hiltunen research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240262,Juffer and Ohlmeier research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240262,2402620,Juffer and Ohlmeier research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240263,Karppinen research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240263,240002630,Karppinen research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240264,Kastaniotis research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240264,2402640,Kastaniotis research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240265,Kellokumpu and Glumoff research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240265,2402650,Kellokumpu and Glumoff research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240266,Kietzmann research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240266,2402660,Kietzmann research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240267,Kursula research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240267,2402670,Kursula research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240268,Lehtiö research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240268,2402680,Lehtiö research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240269,Manninen research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240269,2402690,Manninen research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240270,Myllyharju research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240270,2402700,Myllyharju research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240271,Pihlajaniemi and Heljasvaara research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240271,2402710,Pihlajaniemi and Heljasvaara research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240272,Ruddock research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240272,2402720,Ruddock research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240273,Vainio research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240273,2402730,Vainio research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240274,Wei research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240274,2402740,Wei research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240275,Wierenga and Venkatesan research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240275,2402750,Wierenga and Venkatesan research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24051,ARKKITEHTUURIN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24051,240510,Arkkitehtuurin tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240510,2405100,Arkkitehtuurin tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24062,TIETO- JA SÄHKÖTEKNIIKAN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240593,Tieto- ja sähkötekniikan tiedekunnan yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240593,2405930,Tieto- ja sähkötekniikan tiedekunnan yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240593,2405932,TST Tietohallinto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240593,2405933,TST Strateginen rahoitus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240593,2405941,TST Työpaja,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240593,2405942,TST Fablab,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240594,TST Tiedekuntainfra,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240594,2405940,TST Tiedekuntainfra,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240594,2405943,Fablab Infrastruktuuri,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240595,TST Koulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240595,2405950,TST Koulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240595,2405951,Tietotekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240595,2405952,Sähkötekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240595,2405953,Tietojenkäsittelytietieteiden ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240589,Sovellettu ja laskennallinen matematiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240589,2405890,Sovellettu ja laskennallinen matematiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240208,"Empiirinen ohjelmistotuotanto ohjelmistoissa, järjestelmissä ja palveluissa",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240208,2402080,"Empiirinen ohjelmistotuotanto ohjelmistoissa, järjestelmissä ja palveluissa",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240596,Vakuuttavat verkko- ja mobiilipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240596,2405960,Vakuuttavat verkko- ja mobiilipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240597,Käyttöliittymät ja ihmiskeskeinen digitalisaatio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240597,2405970,Käyttöliittymät ja ihmiskeskeinen digitalisaatio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240582,Optoelektroniikka ja mittaustekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240582,2405820,Optoelektroniikka ja mittaustekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240583,Elektroniikan piirit ja järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240583,2405830,Elektroniikan piirit ja järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240586,Mikroelektroniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240586,2405860,Mikroelektroniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240584,Konenäkö ja signaalianalyysi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240584,2405840,Konenäkö ja signaalianalyysi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240585,Biosignaalien analyysi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240585,2405850,Biosignaalien analyysi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240599,Biomimetiikka ja älykkäät järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240599,2405990,Biomimetiikka ja älykkäät järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240611,Jokapaikan tietotekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240611,2406110,Jokapaikan tietotekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240587,CWC - Radioteknologiat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240587,2405870,CWC - Radioteknologiat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240612,CWC - Verkot ja järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240612,2406120,CWC - Verkot ja järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2420,Painoalat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2420,24064,Eudaimonia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24064,240640,Eudaimonia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240640,2406400,Eudaimonia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2420,24066,Biocenter,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24066,240681,Biocenter,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240681,2406810,Biocenter,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2420,24068,Infotech,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24068,240680,Infotech,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240680,2406800,Infotech,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2420,24077,Thule-instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24077,240770,Thule-instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240770,2407700,Thule-instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240770,2407701,Arktinen lääketiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240770,2407702,Uarctic toimisto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2430,Alueyksiköt,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2430,24073,Kerttu Saalasti instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240730,Kerttu Saalasti instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240730,2407300,Kerttu Saalasti instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240731,KSI Aluekehittäminen,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240731,2407310,KSI Aluekehittäminen,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240732,KSI Tulevaisuuden tuotantoteknologiat (FMT),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240732,2407320,KSI Tulevaisuuden tuotantoteknologiat (FMT),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240733,KSI Mikroyrittäjyys (MikroY),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240733,2407330,KSI Mikroyrittäjyys (MikroY),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240734,KSI Maanalainen fysiikka (CUPP),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240734,2407340,KSI Maanalainen fysiikka (CUPP),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240736,KSI Raahe,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240736,2407360,KSI Raahe,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2430,24076,Sodankylän geofysiikan observatorio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24076,240760,Sodankylän geofysiikan observatorio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240760,2407600,Sodankylän geofysiikan observatorio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24076,240762,SGO Tutkimus- ja kehitystoiminta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240762,2407620,SGO Tutkimus- ja kehitystoiminta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2430,24080,Kajaanin yliopistokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24080,240800,Kajaanin yliopistokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240800,2408000,Kajaanin yliopistokeskus yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24080,240806,CEMIS-Oulu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240806,2408060,CEMIS-Oulu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240806,2408061,Analyyttinen kemia/bioanalytiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240806,2408062,Kuvantavat mittaukset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240806,2408063,Optinen spektroskopia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24080,240807,AIKOPA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240807,2408071,AIKOPA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2440,Erillisyksiköt,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2440,24065,UniOGS,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24065,240650,University of Oulu Graduate School,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240650,2406500,University of Oulu Graduate School,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2440,24081,Kirjasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24081,240810,Kirjasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408100,Kirjasto yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408101,Hankintapalvelu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408102,Tietoaineistopalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408103,Lainaus- ja neuvontapalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408104,Koulutus- ja tietopalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408150,Kirjaston hallintopalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2440,24086,Täydentävien opintojen keskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24086,240820,Kieli- ja viestintäkoulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240820,2408200,Kieli- ja viestintäkoulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24086,240840,Aikuiskoulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240840,2408400,Aikuiskoulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240840,2408406,Avoin yliopisto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2445,Infrastruktuuri,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24067,Biocenter Core,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24067,240670,Biocenter Core,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240670,2406700,Biocenter Core,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24079,Mikroskopian ja nanoteknologian keskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24079,240790,Mikroskopian ja nanoteknologian keskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240790,2407900,Mikroskopian ja nanoteknologian keskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24083,Koe-eläinkeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24083,240830,Koe-eläinkeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240830,2408300,Koe-eläinkeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24085,Oulangan tutkimusasema,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24085,240771,Oulangan tutkimusasema,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240771,2407710,Oulangan tutkimusasema,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24088,LEAF,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24088,240778,LEAF,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240778,2407780,LEAF,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24089,Biodiversiteettiyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24089,240212,Biodiversiteettiyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240212,2402120,Biodiversiteettiyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2450,Yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2450,24091,Yliopiston yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24091,240910,Yliopiston yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240910,2409100,Yliopiston yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24091,240911,Kiinteistöt,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409110,Kiinteistöt,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409115,Tilanvaraustilat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409125,Autopaikat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409130,Rakentaminen ja kunnossapito,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409135,Turvallisuus ja ympäristö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409140,Aulapalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409150,Postipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409155,Kalusteasentajat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409160,Hankintapalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24091,240917,Strategiarahat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240917,2409170,Strategiarahat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24091,240918,YO Tietohallinto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240918,2409180,YO Lisenssit,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240918,2409181,YO Infrastruktuuri,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240918,2409182,YO Tietojärjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24091,240901,YO Koulutuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240901,2409010,YO Koulutuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2460,Johto ja palvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2460,24092,Johto ja palvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240920,YK-hyvitykset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240920,2409200,YK-hyvitykset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240900,Rehtoraatin yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240900,2409000,Rehtoraatin yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240921,Johto ja palvelut - yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240921,2409210,Johto ja palvelut - yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240922,Henkilöstöpalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240922,2409220,Henkilöstöpalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240923,Koulutuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409230,Resurssipooli,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409232,Opintotuki (entinen työelämäpalvelut) 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409236,Opetuksen tuki,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409237,Opiskelijavalinta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409238,Opiskeluoikeuden ylläpito,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409239,Opiskelun tuki,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240924,Talouspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240924,2409240,Talouspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240925,Tila- ja turvallisuuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240925,2409250,Tila- ja turvallisuuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240925,2409251,Virastomestaripalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240927,"Viestintä, markkinointi ja yhteiskuntasuhteet",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240927,2409270,"Viestintä, markkinointi ja yhteiskuntasuhteet",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240928,Tutkimuksen strategiset palvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240928,2409280,Tutkimuksen strategiset palvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240930,Tietohallintopalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409300,Tietohallintopalvelut yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409305,Asiakirjapalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409360,Tietojärjestelmäpalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409380,Infrapalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409385,Opetuksen ja tutkimuksen IT-tukipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409390,Asiakaspalvelu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240941,Palvelupisteet henkilöstöhallinto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240941,2409471,Henkilöstöhallinto keskitetyt palvelut (KEPA),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240942,Palvelupisteet koulutuspalvelut 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409412,Koulutuspalvelut HKT 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409422,Koulutuspalvelut LUTK 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409432,Koulutuspalvelut Kontinkangas 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409442,Koulutuspalvelut TTK 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240943,Palvelupisteet talouspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240943,2409433,Taloushallinto Kontinkangas,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240943,2409473,Taloushallinto keskitetyt talouspalvelut palvelupiste,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240948,Tutkimuksen tukipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240948,2409480,Tutkimuksen tukipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240949,Innovaatio- ja yrittäjyyspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240949,2409245,Innovaatio- ja yrittäjyyspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240720,Center for Health and Technology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240720,2407200,Center for Health and Technology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240775,CEE Innovaatiokeskittymä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240775,2407750,CEE Innovaatiokeskittymä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240531,Tuotantotekniikka 31.12.2015 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240773,NorTech 31.12.2015 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240700,CIE 31.12.2015 saakka,, +Poliisiammattikorkeakoulu,Police University College,Polisyrkeshögskolan,02557,,,,http://isni.org/isni/0000000097577818,1602 +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,,,http://isni.org/isni/0000000404184038,31 +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2017,Opiskelijapalvelut ja opetuksen yhteiset,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2040,TKI- ja palvelutoiminta,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2099,AMK yhteiset palvelut,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2100,Kulttuuri,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2200,Liiketalous,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2310,Hotelli- ja ravintola-ala,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2500,Tekniikka,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2400,Sosiaali- ja terveysala,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2900,Kielikeskus,, +Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,,,http://isni.org/isni/0000000101657504,1748 +Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,TECH,Teknologia,, +Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,HYVO,Hyvinvointi,, +Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,LOME,Logistiikka ja meriteknologia,, +Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,OPET,Opetuspalvelut,, +Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,PATA,Palveluliiketoiminta,, +Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,RETO,Rehtorin toimisto,, +Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,TAHA,Talous ja hallinto,, +Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,TKI,TKI-palvelut,, +Savonia,University of Applied Sciences Savonia,,02537,,,,http://isni.org/isni/0000000404176521,1759 +Savonia,University of Applied Sciences Savonia,,02537,,100,LiTe vastuualueen yhteiset,, +Savonia,University of Applied Sciences Savonia,,02537,,110,Teknologia ja ympäristöala,, +Savonia,University of Applied Sciences Savonia,,02537,,300,Liiketalousala,, +Savonia,University of Applied Sciences Savonia,,02537,,350,Matkailu- ja ravitsemisala,, +Savonia,University of Applied Sciences Savonia,,02537,,400,Hyvin vastuualueen yhteiset,, +Savonia,University of Applied Sciences Savonia,,02537,,410,Sosiaali- ja terveysala,, +Savonia,University of Applied Sciences Savonia,,02537,,500,Kulttuuriala,, +Savonia,University of Applied Sciences Savonia,,02537,,600,Luonnonvara-ala,, +Savonia,University of Applied Sciences Savonia,,02537,,800,Savonia liiketoiminta,, +Savonia,University of Applied Sciences Savonia,,02537,,900,Hallinto- ja korkeakoulupalvelut,, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,,,,http://isni.org/isni/0000000104772049,U2VpbsOkam9lbiBhbW1hdHRpa29ya2Vha291bHU= +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,ElMa,ElMa,SeAMK Elintarvike ja maatalous,, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,ElMa,MaMe,Luonnonvara (maa- & metsätalousalat),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,ElMa,BiEli,Bio- ja elintarviketekniikka (tekniikka),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,ElMa,Resto,Ravitsemisala (palveluala),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,LiKu,LiKu,SeAMK Liiketoiminta ja kulttuuri,, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,LiKu,Liike,"Liiketalous (kauppa, hallinto & oikeus)",, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,LiKu,Kult,Kulttuuri (taiteet ja kulttuuri),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,LiKu,KiTi,Kirjasto ja tietopalvelu (yhteiskunnalliset alat),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,SosTer,SosTer,SeAMK Sosiaali- ja terveysala (terveys & hyvinvointi),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,Tekn,Tekn,SeAMK Tekniikka,, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,Tekn,Tite,Tietotekniikka (tietojenkäsittely & tietoliikenne),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,Tsto,Tsto,SeAMK Toimisto,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,,,http://isni.org/isni/0000000111777737,SGFua2VuIFN2ZW5za2EgaGFuZGVsc2jDtmdza29sYW4= +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,1,Förvaltningsämbetet,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,2,Institutionen för finansiell ekonomi,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,3,Institutionen för företagsledning och organisation,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,4,Institutionen för redovisning och handelsrätt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,5,Institutionen för marknadsföring,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,6,Institutionen för nationalekonomi,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,7,Biblioteket,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,8,Centret för forskning och internationella ärenden,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,9,Datacentralen,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,10,Hanken Fortbildning,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,11,Centret för språk och affärskommunikation,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,19,Samfinansierade enheter,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,20,Fristående institutioner,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,7,3211,Biblioteket - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,20,3212,Tritonia,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,8,3311,Centret för forskning och internationella ärenden,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,8,3331,Forskningsservice,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,9,3411,Datacentralen - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,9,3412,Datacentralen - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,2,4111,Finansiell ekonomi - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,2,4112,Finansiell ekonomi - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,2,4121,Statistik - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,2,4122,Statistik - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4211,Företagsledning och organisation - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4212,Företagsledning och organisation - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4221,Entreprenörskap och företagsledning - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4222,Entreprenörskap och företagsledning - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4231,Informationsbehandling - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4232,Informationsbehandling - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,4311,Handelsrätt - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,4312,Handelsrätt - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,4321,Redovisning - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,4322,Redovisning - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,4411,Marknadsföring - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,4412,Marknadsföring - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,4421,Logistik och samhällsansvar,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,4511,Nationalekonomi - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,4512,Nationalekonomi - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4705,Centret för språk,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4711,Svenska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4712,Svenska -Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4721,Finska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4722,Finska - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4731,Engelska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4732,Engelska - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4741,Tyska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4742,Tyska - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4751,Franska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4761,Spanska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4771,Ryska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7311,Öppna universitetet - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7312,Öppna universitetet - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7411,Rektorsämbetet - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7421,Allmän adm. och personalärenden - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7422,Allmän adm. och personalärenden - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7431,Studiebyrån - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7432,Studiebyrån - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7451,Marknadsföring och information - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7452,Marknadsföring och information - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7453,Karriärtjänster - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7455,Karriärtjänster - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7456,Alumni - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7458,Partnerskap,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7459,Fundraising,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7461,Ekonomiavdelningen,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7472,Näringslivskoordinator - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7491,Fastighetspersonal och infra - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7492,Fastighetspersonal och infra - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7511,Fastigheter och servicepersonal - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7721,Helsingforsalliansen,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,10,8316,"MBA, Modulkostnader",, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,10,8317,MBA,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,8,8331,Centret för forskn.o.int.är./projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,19,8351,KATAJA,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,2,8411,Finansiell ekonomi / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,8412,Statistik / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,8421,Företagsledning och organisation / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,8422,Entreprenörskap och företagsledning / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,8423,Informationsbehandling / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,8431,Handelsrätt / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,8432,Redovisning / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,8441,Marknadsföring / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,8442,Logistik och samhällsansvar /projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,8451,Nationalekonomi / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,8471,Samtliga språk / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,10,8511,Hanken fortbildning Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,8521,IPR University Center,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,8531,CERS - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,8535,HUMLOG Institute,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,8565,Hanken Centre for Corporate Governance,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,8742,Allmän adm. och personalärenden / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,8743,Studiebyrån / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7321,Samlad service,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,8745,Marknadsföring och information / projekt,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,,,http://isni.org/isni/0000000417907610,1942 +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1260,KuvA/Johdon tuki,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1270,KuvA/Näyttelytoiminnan tuki,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1380,KuvA/Kv-avaukset,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1590,KuvA/Kirjasto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2201,TeaK/Ohjauksen koulutusohjelma,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2231,TeaK/Tanssin koulutusohjelma BA,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2822,TeaK/Opetuksen suunnitt ja keh,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2832,TeaK/Näyttämö ja tarpeisto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31160,SibA/Musiikkikasvatuksen aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3740,37410,Konserttivahtimestarit,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3750,SibA/Esitystekniikkapalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3801,SibA/Varadekaani I,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3830,SibA/Primo ohjelmapalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7100,Viestintäpalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7400,Opintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8420,Avoin yliopisto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1100,KuvA/Johto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1130,KuvA/Viestintä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1210,KuvA/Opintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1220,KuvA/Tekniset palvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1250,KuvA/Henkilöstöpalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1300,KuvA/Opiskelun tuki,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1310,KuvA/Taidegrafiikka,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1320,KuvA/Kuvanveisto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1330,KuvA/Tila-aikataide,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1340,KuvA/Maalaustaide,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1360,KuvA/Yhteisen opetuksen opetusalue,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1390,KuvA/Esittämiskäytännöt ja tilallisuus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1410,KuvA/Tohtorikoulutusohjelma,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1430,KuvA/Taiteellinen tutkimus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1500,KuvA/Nykytaiteen laboratorio,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1510,KuvA/Näyttelytoiminta,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2101,TeaK/Dekaani,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2200,TeaK/Näyttelijätyön koulutusoh,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2202,TeaK/Dramaturgian koulutusohj,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2203,TeaK/Live Art and Performance S,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2210,TeaK/MA in Ecology and Contemporary Performance,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2220,TeaK/Utbildingsprog. för skådespelarkonst,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2232,TeaK/Tanssijan maisteriohjelma,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2233,TeaK/Koreografin maisteriohj,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2241,TeaK/Valosuunnittelun koulutus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2242,TeaK/Äänisuunnittelun koulutus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2251,TeaK/Tanssiopettajan koulutus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2252,TeaK/Teatteriopettajan koulutus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2260,TeaK/Esittävien Taiteiden Tutk,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2270,TeaK/Yhteisen opetuksen keskus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2811,TeaK/Hallintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2812,TeaK/Viestintä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2821,TeaK/Opintohallinto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2823,TeaK/Kirjasto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2830,TeaK/Opetusteatteri,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2831,TeaK/Esitystuotanto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2833,TeaK/Puvusto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2834,TeaK/Valo ja ääni,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,3111,"SibA/Musiikkikasvatuksen, jazzin ja kansanmusiikin osasto",, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31120,SibA/Jazzin aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31130,SibA/Kansanmusiikin aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31165,SibA/Taidehallinnon aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31190,SibA/Musiikkiteknologian aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31270,SibA/MuTri-tohtorikoulu,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32140,"SibA/Kirkkomusiikin ja urkujen aineryhmä, Hki",, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32150,"SibA/Kirkkomusiikin ja urkujen aineryhmä, Kuopio",, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32170,SibA/Laulun ja korrepetition aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32180,SibA/Sävellyksen ja musiikinteorian aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32210,SibA/Pianon aineryhmä (piano+kantele+kitara+harmonikka),, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,3222,SibA/Klassisen musiikin osasto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32230,"SibA/Orkesteri-, kamarimusiikki- ja kapellimestarikoulutuksen aineryhmä",, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32240,SibA/Jousten aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32250,SibA/Puhaltimien aineryhmä (lyömäsoittimet+harppu),, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32260,SibA/Vanhan musiikin aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32270,SibA/DocMus-tohtorikoulu,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,3251,SibA/Kuopion hallinto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,3252,SibA/Kuopion kirjasto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,3400,SibA/Seinäjoen toimipaikka,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3600,SibA/Kirjasto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3705,SibA/Henkilöstö- ja johdon tukipalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3710,SibA/Viestintäpalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3730,SibA/Soitinten huolto ja hankinnat,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3740,3740,SibA/Tuotantopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3780,SibA/Keskitetyt opintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,37810,"Musiikkikasvatuksen, jazzin ja kansanmusiikin opintopalvelut",, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,37820,Klassisen musiikin opintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,37830,Orkesterikoulutuksen opintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3800,SibA/Dekaani,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3802,SibA/Varadekaani II,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3811,SibA/Kehittämiskeskus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3814,SibA/Tohtoriohjelma,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3817,SibA/Kansainväliset asiat,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3825,SibA/Yritysyhteistyö,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7000,Henkilöstö- ja johdon tukipalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7110,Varainhankinta,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7200,Talouspalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7300,IT/Tietohallinto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7310,IT/Sovellus- ja infrapalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7330,IT/Tukipalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7600,Toimitilapalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7610,Kallio-Kuninkala,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7690,N-talohanke,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7800,Kirjasto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8000,Rehtoraatti,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8200,Opetus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8210,Kielten opetus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8300,Jatkokoulutus ja tutkimus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8400,Avoin kampus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8410,Täydennyskoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,,,,http://isni.org/isni/0000000103468395,36 +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311257,Media-alan koulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H2,311003,Henkilöstöpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311004,Talous- ja projektipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H2,311007,Matkapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311013,Laadunhallinta ja toiminnanohjaus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311014,Floworks,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311017,Kehittämisyksikkö,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311021,Hallintopalvelut (ja tapahtumapalvelut),, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311022,Liikuntapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311030,Hankintapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311031,Viestintäpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311040,Kiinteistöpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311041,Tilapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311050,Kirjasto- ja tietopalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311060,Tietohallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311080,Opintopalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311090,Kansainväliset palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311110,Avoin amk,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311112,Kielipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311250,"Musiikin ko, muusikko, Musiikkipedagogi",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311252,Kuvataiteen ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311255,Degree Programme in Media and Arts,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311256,Mediatuottamisen ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311259,MDP in Screenwriting YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311260,"Musiikin ko, Musiikkipedagogi YAMK",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311300,Liiketalouden ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311301,Degree Programme in International Business,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311302,Yrittäjyyden ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311309,Yrittäjyyden ja tiimijohtamisen ko / Proakatemia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311350,Tietojenkäsittelyn ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311351,Tietojärjestelmäosaamisen ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311401,Konetekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311402,Ajoneuvotekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311403,Rakennus- ja yhdyskuntatekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311404,Sähkö- ja automaatiotekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311405,Tieto- ja viestintätekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311406,"Talotekniikan ko , sähköinen talotekniikka, LVI-tekniikka",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311407,Rakennusalan työnjohdon ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311408,Biotuote- ja prosessitekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311409,Laboratoriotekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311410,Degree Programme in Energy and Environmental Engineering,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311411,Rakentamisen ja talotekniikan ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311413,Teknologiaosaamisen johtamisen ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311414,Automaatioteknologia YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311423,Rakennusarkkitehti,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311444,Matematiikka,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311445,Fysiikka,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311450,Metsätalouden ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311456,Master's Degree Programme in Management and Economy in the International Forest Sector,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311502,Sairaanhoitajakoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311503,Sosionomikoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311504,Ensihoitajakoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311505,Bioanalytiikan ko / Bioanalyytikko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311507,Fysioterapian ko / Fysioterapeutti,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311510,Röntgenhoitajakoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311514,Hyvinvointiteknologian ko / YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311516,Sosiaali- ja terveysalan johtamisen koulutus (YAMK),, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311517,Sosionomin koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311518,Terveyden edistämisen koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311524,Terveydenhoitajakoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311529,Kliinisen asiantuntijan koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311551,Palveluliiketoiminnan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311558,Palvelu- ja projektiliiketoiminnan ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311559,Palveluliiketoiminnan johtamisen koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311599,Kätilökoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311600,TAMK Ammatillinen opettajankoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311831,Ammattipedagoginen TKI,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311921,Master´s Degree in Information Technology,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311925,Työelämäpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,,H1,Rehtorin toimisto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H2,Henkilöstöhallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H3,Liiketoiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H4,Korkeakoulupalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H5,Koulutus ja TKI-toiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H55,Terveys- ja sosiaalipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H51,"Taide, musiikki ja media",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H52,Liiketoiminta ja palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H53,Rakentaminen ja ympäristöteknologia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H54,Teollisuusteknologia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H6,Kehittäminen,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,,,,http://isni.org/isni/0000000093279856,1972 +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,220,Yleispalvelujen yhteiset,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,221,Strateginen johtaminen,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,223,Viestintä,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,224,Kumppanuudet,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,225,Talous ja toiminnan ohjaus,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,226,Henkilöstöpalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,227,Tietohallinto,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,228,Tilapalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,23,231,Oppimisen tuki,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,23,232,Tutkintopalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,23,233,Opintopalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,24,241,Tutkimuksen kehittämispalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,24,242,Tutkimuspalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,24,243,Innovaatiopalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,24,244,Kirjasto,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,25,251,Tampere3,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,610,Luonnontieteiden tiedekunta,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,611,Fysiikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,612,Kemian ja biotekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,613,Matematiikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,614,Optoelektroniikan tutkimuslaitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,710,Teknisten tieteiden tiedekunta,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,711,Hydrauliikan ja automatiikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,712,Materiaaliopin laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,713,Systeemitekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,715,Kone- ja tuotantotekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,810,Tieto- ja sähkötekniikan tiedekunta,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,811,Elektroniikan ja tietoliikennetekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,812,Signaalinkäsittelyn laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,813,Sähkötekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,814,Tietotekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,910,Talouden ja rakentamisen tiedekunta,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,911,Arkkitehtuurin laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,912,Rakennustekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,913,Kielikeskus,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,914,Porin laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,915,Teollisuustalouden laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,916,Tiedonhallinnan ja logistiikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,918,Täydennyskoulutuskeskus Edutech,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,,,http://isni.org/isni/0000000123146254,37 +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2501,BioMediTech,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2502,Informaatiotieteiden yksikkö,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2503,Johtamiskorkeakoulu,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,02504,Kasvatustieteiden yksikkö,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2505,"Kieli-, käännös- ja kirjallisuustieteiden yksikkö",, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2506,Lääketieteen yksikkö,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,02507,"Viestinnän, median ja teatterin yksikkö",, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2508,Terveystieteiden yksikkö,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,02509,Yhteiskunta- ja kulttuuritieteiden yksikkö,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2580,Laboratoriopalvelut,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2571,Yhteiskuntatieteellinen tietoarkisto,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2572,Kielikeskus,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2573,Kirjasto,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2592,Yliopistopalvelut,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,,,,http://isni.org/isni/0000000404747718,2131 +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9611,961111,Yhteiset palvelut,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962011,"Liiketalous, ICT ja kemiantekniikka, yht.",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962012,Liiketalous,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962013,ICT,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962014,Kemiantekniikka,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962015,LIKe monimuotokoulutukset ja YAMK,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963011,"Taideakatemia, yht.",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963012,"Taideakatemia, projektit",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963013,Esittävät taiteet,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963014,Kuvataide,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963015,Media-ala,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963016,Taideakatemia monimuotokoulutukset ja YAMK,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964011,"Tekniikka, ympäristö ja talous, yht.",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964012,"Konetekniikka, meritekniikka ja muotoilu",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964013,"Rakentaminen, ympäristö ja energia",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964014,"Hankinnat, myynti ja logistiikka",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964015,TYT monimuotokoulutukset ja YAMK,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965011,"Terveys ja hyvinvointi, yht.",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965012,Terveysala,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965013,Kuntoutus ja terveysalan erityisalueet,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965014,Sosiaaliala,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965015,Terhy monimuotokoulutukset ja YAMK,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,,,http://isni.org/isni/0000000121981512,41 +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601000,Johto ja yliopiston yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601004,Muutostuki,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601011,Kiinanmylly,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601012,Seili,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601100,Talouspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601105,Reskontrapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601106,Matkapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601107,Projektipalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601200,UTUGS,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601201,Tutkijatohtoriohjelma,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601210,LLK:n yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601211,LLK Matematiikan ja tilastotieteen lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601212,LLK Biokemian laitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601213,LLK Kemian lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601214,LLK Biologian lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601215,LLK Fysiikan ja tähtitieteen lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601216,LLK Informaatioteknologian lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601217,LLK Maantieteen ja geologian lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601218,LLK Biolääketieteen lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601219,LLK Kliininen laitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601220,LLK Hoitotieteen lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601221,LLK Hammaslääketieteen lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601230,TIAS-tutkijakollegiumin yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601231,TIAS Humanistinen tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601232,TIAS Yhteiskuntatieteellinen tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601233,TIAS Kasvatustieteiden tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601234,TIAS Oikeustieteellinen tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601235,TIAS Kauppakorkeakoulu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601240,Tutkimuksen toimialan yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601241,Tutkimuksen kehittäminen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601242,Tutkimuspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601250,SKY yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601251,Innovaatiopalvelut ja koulutusvienti,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601252,Kehittäminen ja suunnittelu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601260,TCSI yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601261,TCSI International,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601300,Viestintä,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601400,Henkilöstöpalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601401,IT-palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601405,Yleispalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601406,Palvelujohtajan yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601410,Koulutuksen toimialan yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601411,Kansainväliset palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601412,Ohjauksen ja koulutuksen tukipalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601413,Opiskelija- ja hakijapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601414,Liikuntapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601415,Koulutuksen toimiala tiedekunnissa,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601416,Avoin yliopisto 1.8. alkaen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601420,Kirjaston yhteiset palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601421,Oppimisen palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601422,Tutkimuksen palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601423,Tietoaineistojen saatavuuspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601424,Hankintapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601430,Toimitilapalvelujen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601431,Tilasuunnittelu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601432,Tila- ja kuljetuspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601433,Majoituspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601434,Protopaja,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601435,Yliopiston yhteiset tilat,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601436,Turvallisuus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601510,Vyörytettävät kustannukset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601611,Drug Development and Diagnostics,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601621,Learning and Education,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601631,Bioimaging,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601641,Digital Futures,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601651,New Bioresources,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601900,Rahastot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601930,Tiedekuntien stipendirahasto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602000,Humanistisen tiedekunnan yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602001,Humanistisen tiedekunnan hallintopalvelu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602002,Baltic Sea Regions Studies,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602100,Kieli- ja käännöstieteiden laitoksen yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602101,Englannin kieli,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602102,Espanja,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602103,Fonetiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602104,Italia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602105,Klassiset kielet ja antiikin kulttuuri,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602106,Pohjoismaiset kielet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602107,Ranska,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602108,Saksan kieli,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602109,Volgalaiskielten tutkimusyksikkö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602110,Suomen kieli ja suom-ugrilainen kielent,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602111,Venäjän kieli,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602114,Lauseopin arkisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602200,"Historian, kultt ja tait tutk lts:n yht",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602201,Arkeologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602202,Folkloristiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602203,Kansatiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602204,Kotimainen kirjallisuus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602205,Kulttuurihistoria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602206,Mediatutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602207,Musiikkitiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602208,Sukupuolentutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602209,Suomen historia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602210,Taidehistoria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602211,Uskontotiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602212,Yleinen historia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602213,Yleinen kirjallisuustiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602214,Kulttuurituotannon ja maisemantutkim ko,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602215,Kulttuurientutkimuksen arkisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602216,Kalevala instituutti,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602217,Luova kirjoittaminen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602218,Museologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603000,Yhteisk tdk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603001,Yhteisk tdk hallintopalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603100,Psykologian ja logopedian laitoksen yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603101,Filosofia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603102,Logopedia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603103,Psykologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603104,PSYKONET -verkosto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603105,Kognitiivisen neurotieteen tutk yks (KNT,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603106,Oppimistutkimuksen keskus (OTUK),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603107,KiVa Koulu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603111,KiVa Koulu elinkeinotoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603200,"Filosofian, poliittisen historian ja valtio-opin lts yht",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603201,Poliittinen historia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603202,Valtio-oppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603203,Eduskuntatutkimuksen keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603204,Itä-Aasian tutkimus- ja koulutusk (CEAS),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603205,Yliopistojen Aasia-verkosto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603206,The Public Choice Research Centre (PCRC),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603207,John Morton -keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603208,Filosofia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603300,Sosiaalitieteiden laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603301,Sosiaalipolitiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603302,Sosiaalityö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603303,Sosiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603304,Taloussosiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603305,Sosiaalivak huippuas jatkok ohj (TOPSOS),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603306,SOSNET-verkosto (sosiaalityö),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604000,Kasv tdk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604001,Kasv tdk tiedekuntapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604002,Oppimistutkimuksen keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604021,Et Koulutusvienti,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604100,Kasvatustieteiden laitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604200,Opettajankoulutuslaitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604201,OKL Turku,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604202,OKL Rauma,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604300,Turun normaalikoulun yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604301,Lukiokoulutus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604302,Perusopetus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604303,Opettajankoulutus ja kehitystehtävät,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604400,Rauman normaalikoulun yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604401,Perusopetus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604402,Opettajankoulutus ja kehitystehtävät,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2605000,Oikeustiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2605100,Oikeustieteellisen liiketoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606000,Mat luonn tdk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606001,Mat luonn tdk hallintopalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606010,TYYK:n yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606011,Lapin tutkimuslaitos Kevo,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606012,Saaristomeren tutkimuslaitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606013,Aerobiologian yksikkö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606100,Matematiikan ja tilastotieteen lait yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606101,Matematiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606102,Sovellettu matematiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606103,Tilastotiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606200,Biokemian laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606201,Biokemia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606202,Biotekniikka / FM,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606203,Biotekniikka / DI,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606204,Elintarvikekemia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606205,Molekulaarinen kasvibiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606206,IFDRC,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606300,Kemian laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606301,Kemian perusopetus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606302,Materiaalikemia ja kemiallinen analyysi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606303,Orgaaninen kemia ja kemiallinen biologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606304,Laitekeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606305,JBL-laboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606400,Biologian laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606401,Biodiversiteettitutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606402,Ekologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606403,Ympäristötiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606404,Genetiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606405,Eläinfysiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606406,Eläinmuseo,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606407,Kasvimuseo,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606408,Kasvitieteellinen puutarha,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606409,Evoluutiobiologian sovelluskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606700,Fysiikan ja tähtitieteen laitoksen yhtei,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606701,Wihurin fysiikantutkimuslaboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606702,Avaruustutkimuslaboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606703,Teoreettinen fysiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606704,Teollisuusfysiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606705,Tuorlan observatorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606706,Materiaalitutkimuksen laboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606707,Kvanttioptiikan laboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606800,Informaatioteknologian laitoksen yhteise,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606801,Tietoliikennetekniikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606802,Sulautettu elektroniikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606803,Tietojenkäsittelytiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606804,Ohjelmistotekniikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606805,Bioinformatiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606806,Vuorovaikutussuunnittelu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606900,Maantieteen ja geologian laitoksen yht.,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606901,Maantiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606902,Geologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607000,Lääket tdk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607001,Lääket tdk hallintopalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607002,Lääket tdk täydennyskoulutus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607003,MediCity,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607004,Sydäntutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607005,Biopankki,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607010,Turun lapsi- ja nuorisotutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607020,Funktionaalisten elint. kehittämiskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607030,Koe-eläinkeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607040,Et Aistila,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607100,Biolääketieteen laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607101,Solubiologia ja anatomia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607102,"Farmakologia, lääkekehitys ja lääkehoito",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607103,Fysiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607104,Lääketieteellinen biokemia ja genetiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607105,Lääketietee mikrobiologia ja immunologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607107,Patologia ja oikeuslääketiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607108,Virusoppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607160,Oikeuslääketieteen maksupalveluyksikkö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607300,Kliinisen laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607301,Anestesiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607302,Biostatistiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607303,Diagnostinen radiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607304,Geriatria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607305,Iho- ja sukupuolitautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607306,Infektiotautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607307,Kansanterveystiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607308,Keuhkosairausoppi ja kliin. allergologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607309,Kirurgia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607310,Ortopedia ja traumatologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607311,Kliininen kemia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607312,"Korva-, nenä-, ja kurkkutautioppi",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607313,Lastentautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607314,Neurologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607315,Kliininen syöpätautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607316,Psykiatria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607317,Silmätautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607318,Sisätautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607319,Synnytys- ja naistentautien oppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607320,Fysiatria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607321,Kliininen neurofysiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607322,Kliininen fysiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607323,Lääketieteen etiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607324,Palliatiivinen lääketiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607325,Lastenneurologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607326,Lastenpsykiatria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607327,Työterveyshuolto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607328,Yleislääketiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607330,Turun lapsi- ja nuorisotutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607400,Hoitotieteen laitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607500,Hammaslääketieteen laitos yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607511,TCBC,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608000,TuKKK:n yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608001,TuKKK:n hallintopalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608002,TuKKK:n viestintä,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608100,Laskentatoimen ja rahoituksen laitos yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608101,Laskentatoimi ja rahoitus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608102,Yritysjuridiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608103,Taloustieteen kvantitatiiviset menetelmä,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608200,Markk ja kv liiketoim laitos yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608201,Markkinointi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608202,Kv. liiketoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608203,Toimitusketjujen johtaminen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608204,Talousmaantiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608210,Pan-Eurooppa Instituutti,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608220,Kielten ja liikeviestinnän yksikkö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608300,Johtamisen ja yrittäjyyden laitos yhteis,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608301,Johtaminen ja organisointi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608303,Tietojärjestelmätiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608310,Entren yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608311,Entre,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608312,Yrittäjyys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608320,"Turun työtieteiden keskus, TCLS",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608400,Taloustiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608610,TSE Exe,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608710,Centre for Collaborative Research (CCR),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608800,Porin tutkintokoulutus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608802,Porin hankerahoitus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608804,Porin yksikön yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608805,Porin täydennyskoulutus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608900,Tulevaisuuden tutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609000,Brahea-keskuksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609001,Sisäiset palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609010,Avoimen yliopiston yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609011,AVO Humanistisen tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609012,AVO Kasvatustieteiden tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609013,AVO Matemaattis-luonnon tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609014,AVO Yhteiskuntatieteellisen tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609015,AVO Lääketieteellisen tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609016,AVO Oikeustieteellisen tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609017,AVO Kauppakorkeakoulun opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609018,AVO Muut koulutukset ja hankkeet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609020,Brahea kehittämispalvelujen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609021,Asiantuntijakehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609022,Aluekehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609023,Yliopistokehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609024,Kongressitoimisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609025,Innovaatio- ja yrityskehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609030,Brahea kehittämispalvelujen elinkein yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609031,Et Avoin yliopisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609032,Et Asiantuntijakehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609033,Et Aluekehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609034,Et Yliopistokehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609035,Et Kongressitoimisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609036,Et innovaatio- ja yrityskehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609040,Mkk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609041,Mkk koulutustoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609042,Mkk Yhteisrahoitteiset T&K-hankkeet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609043,Mkk Kotka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609044,Mkk Meriklusteriohjelma,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609045,Mkk Merifoorumi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609050,Et Mkk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609051,Et Mkk koulutukset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609052,Et Mkk T&K-hankkeet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609053,Et Mkk Kotka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609054,Et Mkk Meriklusteriohjelma,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609055,Et Mkk Merifoorumi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609060,TRC yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609061,Microelectronics,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609062,Embedded and Mixed-Reality Systems,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609063,Biomedical and Environmental ICT,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609200,Biotekniikan keskuksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609201,Biotekniikan keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609210,Mikrosirukeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609211,FMSC Bioinformatiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609240,Proteomics,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609250,Cell Imaging,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609260,BioCity Turku,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609400,Kielikeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609500,Turun tietotekniikan tutkimuskeskus TUCS,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609700,Suomen ESO-keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609810,PET Perustoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609820,PET Tutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609830,COE huippuyksikkö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609900,Koulutussosiologian tutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609901,Rosa-laboratorio,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,,,http://isni.org/isni/0000000406476659,2355 +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,10,Tekniikan yksikkö,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,20,Liiketalouden yksikkö,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,30,Sosiaali- ja terveysalan yksikkö,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,40,MUOVA,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,50,Hallinto ja tukipalvelut,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,60,TKI-yksikkö,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,,,http://isni.org/isni/0000000106722619,2363 +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710050,Nykysuomi ja kääntäminen,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710100,Pohjoismaiset kielet,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710150,Englannin kieli,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710200,Ranskan kieli,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710250,Venäjän kieli,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710300,Saksan kieli ja kirjallisuus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710350,Viestintätieteet,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710400,Aluetiede,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710450,Sosiologia,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710500,Hallintotiede/julkisjohtaminen,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710550,Filosofia,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710600,Julkisoikeus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710650,Sosiaali- ja terveyshallintotiede,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710700,Kielipalvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710900,Filosofinen tdk yht.,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720050,Johtaminen ja organisaatiot,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720100,Laskentatoimi ja rahoitus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720150,Markkinointi,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720200,Taloustiede,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720250,Talousoikeus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720900,Kauppatieteell. tdk yht.,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730050,Automaatiotekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730100,Energiatekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730150,Fysiikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730200,Sähkötekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730250,Matematiikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730300,Talousmatematiikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730350,Tilastotiede,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730400,Tietoliikennetekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730450,Tietotekniikka kauppat.,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730500,Tietotekniikka tekn.,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730600,Tuotantotalous,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730660,Tuotantotalous tekn,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730700,TB Laboratoriot,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730900,Teknillinen tdk. yhteiset,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2735050,SC-Research,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2739050,Vebic omarahoitus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2739100,Vebic taloudellinen toiminta,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2739150,Vebic julkinen rahoitus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740050,Avoin yliopisto,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740100,Johtaminen ja organisaatioiden kehittäminen,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740150,Energia ja ympäristö,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740900,Levón-instituutin yhteiset,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2750050,Tritonia,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760050,Hallitus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760100,Rehtori,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760150,Johdon asiantuntijatuki,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760200,Tutkimuksen palvelut ja tutkijakoulu,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760250,Viestintä ja kumppanuudet,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760300,Henkilöstön palvelut ja ylimmän johdon palvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760350,Talouden ja hankehallinnon palvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760400,Toiminta- taloussuunnittelu,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760450,Opiskelun ja opetuksen palvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760500,Kansainväliset asiat,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760550,"Aulapalvelut, tilat ja turvallisuus",, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760650,Tietohallintopalvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760700,Tietotekniikkapalvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760750,"Lähipalvelut ja johdon palvelut, Fabriikki",, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760800,"Lähipalvelut ja johdon palvelut, Tervahovi",, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2780000,Vaasan yo sijoitustoiminta,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2780050,Viljo Syreniuksen rahasto,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2790050,Yliopiston yhteiset,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2790100,Vaasan korkeakoulukonsortio,, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,,,http://isni.org/isni/0000000404001027,163 +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,11,"HV, Pedagogik",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,111,"HV, Hälsa och välfärd (utom idrott)",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,112,"HV, Idrott",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,212,"EA, Turism",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,25,"EA, Tradenom",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,27,"EA, IT",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,38,"EM, Teknik",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,42,"KK, Media och kultur",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,51,"Övriga, Pedagogik",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,511,"Övriga, HV (utom idrott)",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,512,"Övriga, Service och tjänster",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,52,"Övriga, Konst- och kultur",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,53,"Övriga, Humaniora",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,55,"Övriga, Ekonomi, administration och juridik",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,57,"Övriga, IT",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,58,"Övriga, Teknik",, +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,,,http://isni.org/isni/0000000406476587,2865 +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,FoU,Forskning och utveckling,, +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,TS,Turism och Samhälle,, +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,AD,Administration,, +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,FE,Företagsekonomi,, +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,KU,Kultur,, +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,NB,Naturbruk,, +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,NV,Naturvetenskap,, +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,SF,Sjöfart,, +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,TE,Teknik,, +Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,VS,Vård och sociala,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,,,http://isni.org/isni/0000000121652639,w4VibyBBa2FkZW1p +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801000,FHPT gemensamma kostnader,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801010,FHPT universitetsservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801100,"Kultur, historia och filosofi",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801110,Språk,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801111,Logopedi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801120,Teologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801130,Psykologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801140,Doktorandnätverk Art History,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801141,Doktorandnätverk Old Testament Studies,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801142,Doktorandnätverk AAPL (Abo Akademi Psychology and Logopedics),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801143,"Gamla forskarskolan, FHPT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801144,Doktorandnätverket Minority Research,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801145,Doktorandnätverket The Age of Sweden,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801150,FHPT utbildning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802000,FPV gemensamma kostnader,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802010,FPV universitetsservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802100,Pedagogik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802110,Hälsovetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802120,Socialvetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802130,MediaCity,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802140,"Gamla forskarskolan, FPV",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802150,FPV utbildning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802200,"Vasa övningsskola, gemensamma kostnader",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802210,"Vasa övningsskola, grundutbildning",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802220,"Vasa övningsskola, gymnasiet",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803000,FSE gemensamma kostnader,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803010,FSE universitetsservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803100,Företagsekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803110,Nationalekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803111,Informationsvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803112,Informationssystem,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803113,IAMSR,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803120,Samhällsvetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803121,Rättsvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803122,Institutet för samhällsforskning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803123,Institutet för mänskliga rättigheter,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803140,Doktorandnätverk Realizing Human Rights,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803141,Doktorandnätverk School of Business and Economics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803142,"Gamla forskarskolan, FSE",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803143,Doktorandnätverk Citizens and Demogracy,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803150,FSE utbildning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804000,FNT gemensamma kostnader,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804010,FNT universitetsservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804050,Tekniska serviceenheten,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804100,Naturvetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804110,Kemi (teknisktvetenskapliga),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804111,Kemi (naturvetenskapliga),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804120,Process- och systemteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804121,Energi- och miljöteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804130,Naturmaterialteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804140,Doktorandnätverk Material Research,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804141,Doktorandnätverk Chemical Engineering,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804142,"Gamla forskarskolan, NatVet & teknik",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804150,"Utbildning, naturvetenskaper och teknik",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804160,Naturvetenskap och teknik gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804200,Nationalla PET-centret,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804300,Biokemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804310,Farmaci,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804311,Cellbiologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804312,Husö biologiska station,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804313,Miljö- och marinbiologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804340,Doktorandnätverk Informational and Structural Biology,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804341,Doktorandnätverk Molecular Biosciences,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804342,"Gamla forskarskolan, BioVet",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804343,Doktorandnätverket Functional Marine Biodiversity,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804350,"Utbildning, biovetenskaper",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804360,Biovetenskap gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804400,Bioteknikcentrum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804410,"Gamla forskarskolan, BTC",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804500,Datateknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804510,Datavetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804540,Doktorandnätverk Information Technologies and Mathematics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804541,"Gamla forskarskolan, IT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804550,"Utbildning, informationsteknologi",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804560,IT gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804600,Turku Centre for Computer Science,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804610,"Gamla forskarskolan, TUCS",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2805000,Centret för livslångt lärande,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2805010,Centret för språk och kommunikation,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806000,Åbo Akademis bibliotek,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806010,Tritonia,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806020,Sibeliusmuseum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806030,Sjöhistoriska institutet,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806100,"Allmän universitetsservice, central",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806101,"Allmän universitetsservice, FHPT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806102,"Allmän universitetsservice, FPV",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806103,"Allmän universitetsservice, FSE",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806104,"Allmän universitetsservice, FNT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806110,Ledning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806111,Kommunikation,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806120,Forskningsservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806121,"Utbildningsservice, central",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806122,"Utbildningsservice, FHPT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806123,"Utbildningsservice, FPV",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806124,"Utbildningsservice, FSE",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806125,"Utbildningsservice, FNT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806130,"Ekonomiservice, central",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806131,"Ekonomiservice, FHPT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806132,"Ekonomiservice, FPV",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806133,"Ekonomiservice, FSE",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806134,"Ekonomiservice, FNT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806140,Personalservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806150,Fastighetsservice och upphandling,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806151,Planering,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806152,Fastigheter och hyror,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806160,ICT-service,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,1,Humanistiska fakulteten,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2,Humanistiska fakulteten gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,3,Engelska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,4,Finska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,5,Franska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,6,Ryska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,7,Svenska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,8,Tyska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,9,Filosofi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,10,Etnologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,11,Folkloristik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,12,Historia,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,13,Religionsvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,14,Konstvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,15,Litteraturvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,16,Musikvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,17,Sibeliusmuseum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,18,Sjöhistoriska institutet,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,19,Dnätverk Art History,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,20,Pedagogiska fakulteten,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,21,Pedagogik gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,22,Allmän pedagogik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,23,Barnpedagogik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,24,Specialpedagogik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,25,Vuxenpedagogik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,26,De matematisk-naturvetenskapliga ämnenas och idrottens didaktik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,27,"Pedagogik, tillämpad pedagogik och yrkesundervisningens didaktik",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,28,Slöjdpedagogik och husliga ekonomins didaktik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,29,Språk- och kulturvetenskapernas didaktik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,30,Vasa övningsskola,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,31,VÖS gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,32,"VÖS, åk 1-6",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,33,"VÖS, åk 7-9",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,34,"VÖS, gymnasiet",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,35,Teologiska fakulteten,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,36,Teologiska fakulteten gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,37,Gammaltestamentlig exegetik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,38,Kyrkohistoria,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,39,Teologisk etik och religionsfilosofi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,40,Praktisk teologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,41,Nytestamentlig exegetik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,42,Dogmatik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,43,Dnätverk Old Testament studies,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,44,Institutionen för psykologi och logopedi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,45,Psykologi och logopedi gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,46,Psykologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,47,Logopedi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,48,Dnätverk Psychology and Logopedics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,49,Handelshögskolan,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,50,Handelshögskolan gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,51,Företagsekonomiska ämnena,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,52,Redovisning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,53,Internationell marknadsföring,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,54,Företagets organisation och ledning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,55,Informationsförvaltining,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,56,Nationalekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,57,Dnätverk School of Business and Economics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,58,Socialvetenskapliga institutionen,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,59,Socialvetenskapliga institutionen gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,60,Sociologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,61,Kvinnovetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,62,Vårdvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,63,Socialpolitik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,64,Utvecklingspsykologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,65,Demografi och landsbygdsforskning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,66,Rättsvetenskapliga institutionen,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,67,Rättvetenskaper gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,68,Rättvetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,69,Institutet för mänskliga rättiheter,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,70,Dnätverk Realizing Human Rights,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,71,Statsvetenskapliga institutionen,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,72,Statsvetenskapliga institutionen gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,73,Statskunskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,74,Offentlig förvaltning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,75,Samforsk,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,76,Institutionen för naturvetenskap gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,77,Naturvetenskapliga institutionen,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,78,Matematik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,79,Verkstaden,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,80,Statistik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,81,Fysik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,82,Organisk kemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,83,Fysikalisk kemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,84,Geologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,85,Dnätverk Material Research,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,86,Nationella PET centret,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,87,Nationella PET-Centret,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,88,Institutionen för biovetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,89,Institutionen för biovetenskaper gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,90,Biokemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,91,Farmaci,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,92,Cellbiologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,93,Husö biologiska station,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,94,Miljö- och marinbiologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,95,Biovetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,96,Dnätverk Informational and Structural Biology,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,97,Dnätverk Molecular Biosciences,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,98,Bioteknikcentrum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,99,Bioteknikcentrum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,100,Institutionen för informationsteknologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,101,Instit. för informationsteknologi gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,102,Datateknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,103,Datavetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,104,Informationssystem,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,105,Institute for Advanced Management System Research (IAMSR),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,106,Dnätverk IT and Mathematics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,107,TUCS,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,108,Åbo datatekniska forsknings- och utbildningscentrum (TUCS),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,109,Institutionen för kemiteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,110,Insitutionen för kemiteknik gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,111,Analytisk kemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,112,Oorganisk kemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,113,Teknisk polymerkemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,114,Teknisk kemi och reaktionsteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,115,Trä- och papperskemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,116,Fiber- och cellulosateknologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,117,Pappersförädling,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,118,Anläggningsteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,119,Reglerteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,120,Värmeteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,121,Energiteknik Vasa,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,122,Industriell ekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,123,Dnätverk Chemical Engineering,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,124,Centret för livslångt lärande,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,125,Centret för livslångt lärande (CLL),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,126,Mediacity,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,127,Mediacity,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,128,Centret för språk och kommunikation,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,129,Centret för språk och kommunikation (CSK),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,130,Datacentralen,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,131,Datacentralen,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,132,Åbo Akademis bibliotek,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,133,ÅAB,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,134,Tritonia,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,135,Tritonia 2011,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,136,ÅA gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,137,ÅA gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,138,Fastigheter,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,139,Rektor,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,140,Kansler,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,141,Förvaltningen gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,142,Ekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,143,Forskning och utbildning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,144,Kommunikation,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,145,Personal,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,146,Teknisk service,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,147,Reform 2015,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,,,http://isni.org/isni/0000000123753425,7 +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,5040300,Tulosyksiköt,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030001,Geoenergia GNR,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030002,Yhdyskunnat ja rakentaminen YRA,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030003,Kalliorakentaminen ja sijoituspaikat KAS,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030004,Ympäristögeologia YMP,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030005,Merigeologia MRG,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030006,Mineraalitalous ja malmigeologia MIM,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030007,Mineraalivarannot MIV,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030008,Teollisuusmineraalit TMI,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030009,Mineraalitekniikka ja materiaalit MMA,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030010,Turvevarannot TUR,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030011,Pohjavesi PVI,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030012,Geofysiikan sovellukset GSO,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030013,Tuotantoympäristöt ja kierrätys TUK,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030014,Alueellinen geotieto ALG,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030015,Geotietovarannon hallinta GEH,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030016,Digitaaliset tuotteet ja palvelut DIP,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040200,5040200,Toiminnan johtaminen,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040200,50402003,Strateginen johto,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200310,"Henkilöstö, osaaminen ja työympäristöt",, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200320,Viestintä ja markkinointi,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200330,Strategia ja suunnittelu,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200340,Tieteellinen tutkimus,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200350,Digitaalisuus ja tietovarannot,, +Maanmittauslaitos,National Land Survey of Finland,Lantmäteriverket,4020217,,,,http://isni.org/isni/000000040494894X,1229 +Metsäntutkimuslaitos,Finnish Forest Research Institute,Skogsforskningsinstitutet,404001,,,,http://isni.org/isni/0000000122651136,TWV0c8OkbnR1dGtpbXVzbGFpdG9z +Suomen Pankki,Suomen Pankki,Finlands bank,02022481,,,,http://isni.org/isni/0000000404104982,1888 +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,,,http://isni.org/isni/0000000110191419,1903 +Säteilyturvakeskus,Radiation and Nuclear Safety Authority,Strålsäkerhetscentralen,5550012,,,,http://isni.org/isni/000000011534674X,U8OkdGVpbHl0dXJ2YWtlc2t1cw== +Teknologiantutkimuskeskus VTT Oy,VTT Technical Research Centre of Finland Ltd,Teknologiska forskningscentralen VTT AB,26473754,,,,http://isni.org/isni/0000000121061548,2045 +Terveyden ja hyvinvoinninlaitos,National Institute for Health and Welfare,Institutet för hälsa och välfärd,5610017,,,,http://isni.org/isni/0000000110130499,2060 +Työterveyslaitos,Finnish Institute of Occupational Health,Arbetshälsoinstitutet,02202669,,,,http://isni.org/isni/0000000404105926,VhnDtnRlcnZleXNsYWl0b3M= +Ulkopoliittinen instituutti,Finnish Institute of International Affairs,Utrikespolitiska institutet,1120017,,,,http://isni.org/isni/0000000406205576,2215 +Valtion taloudellinen tutkimuskeskus,VATT Institute for Economic Research,Statens ekonomiska forskningscentral,3060016,,,,http://isni.org/isni/0000000121621381,2396 +Tilastokeskus,Statistics Finland,Statistikcentralen,02454911,,,,http://isni.org/isni/0000000404105635, +Kotimaisten kielten keskus,Institute for the Languages of Finland,Institutet för de inhemska språken,02458728,,,,http://isni.org/isni/0000000109457291, +Maaseutuvirasto,Agency for Rural Affairs in Finland,Landsbygdsverket,24053596,,,,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,02446292,,,,http://isni.org/isni/0000000446686757, +Ilmatieteen laitos,Finnish Meteorological Insitute,Meterologiska institutet,02446647,,,,http://isni.org/isni/0000000122538678, +Väestörekisterikeskus,Population Register Center,Befolkningsregistercentralen,02454372,,,,, +CSC – Tieteen tietotekniikan keskus Oy,CSC - IT Center for Science Ltd,,09206320,,,,, +Väylävirasto,Finnish Transport Infrastructure Agency,Trafikledsverket,10105471,,,,http://isni.org/isni/0000000405091319, diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/uri_to_wkt.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/uri_to_wkt.json new file mode 100755 index 00000000..d150e759 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/uri_to_wkt.json @@ -0,0 +1,5980 @@ +{ +"http://www.yso.fi/onto/yso/p100490":"POINT(24.07502 62.55286)", +"http://www.yso.fi/onto/yso/p100491":"POINT(25.72483 62.60331)", +"http://www.yso.fi/onto/yso/p101044":"POINT(28.98472222 41.01527778)", +"http://www.yso.fi/onto/yso/p104958":"POINT(25.37911 61.50812)", +"http://www.yso.fi/onto/yso/p104967":"POINT(25.56215 63.11313)", +"http://www.yso.fi/onto/yso/p104968":"POINT(2 47)", +"http://www.yso.fi/onto/yso/p104969":"POINT(20.37534 60.24435)", +"http://www.yso.fi/onto/yso/p104970":"POINT(20.37919 60.228)", +"http://www.yso.fi/onto/yso/p104976":"POINT(28.08735 64.15109)", +"http://www.yso.fi/onto/yso/p104981":"POINT(27.85994 64.27022)", +"http://www.yso.fi/onto/yso/p104983":"POINT(27.633333 -14.116667)", +"http://www.yso.fi/onto/yso/p104984":"POINT(103 35)", +"http://www.yso.fi/onto/yso/p104985":"POINT(24.893333333333 35.309722222222)", +"http://www.yso.fi/onto/yso/p104987":"POINT(26 59)", +"http://www.yso.fi/onto/yso/p104988":"POINT(107.9 14.016666666667)", +"http://www.yso.fi/onto/yso/p104989":"POINT(8 9)", +"http://www.yso.fi/onto/yso/p104990":"POINT(-2 54)", +"http://www.yso.fi/onto/yso/p104995":"POINT(-0.1275 51.507222222222)", +"http://www.yso.fi/onto/yso/p104996":"POINT(2.3518277777778 48.856577777778)", +"http://www.yso.fi/onto/yso/p105005":"", +"http://www.yso.fi/onto/yso/p105006":"POINT(-59.4 -13)", +"http://www.yso.fi/onto/yso/p105007":"", +"http://www.yso.fi/onto/yso/p105010":"POINT(27.78866 62.49183)", +"http://www.yso.fi/onto/yso/p105011":"POINT(28.18406 62.38799)", +"http://www.yso.fi/onto/yso/p105012":"POINT(27.65619 62.45669)", +"http://www.yso.fi/onto/yso/p105013":"POINT(27.94427 62.44993)", +"http://www.yso.fi/onto/yso/p105016":"POINT(26.03108 60.44702)", +"http://www.yso.fi/onto/yso/p105018":"POINT(27.62716 62.89229)", +"http://www.yso.fi/onto/yso/p105019":"POINT(27.67023 61.89949)", +"http://www.yso.fi/onto/yso/p105020":"POINT(27.54088 61.92969)", +"http://www.yso.fi/onto/yso/p105021":"POINT(27.44234 61.90811)", +"http://www.yso.fi/onto/yso/p105023":"POINT(136 35)", +"http://www.yso.fi/onto/yso/p105024":"POINT(86.66666667 46.28333333)", +"http://www.yso.fi/onto/yso/p105025":"POINT(41 29)", +"http://www.yso.fi/onto/yso/p105026":"POINT(22 42)", +"http://www.yso.fi/onto/yso/p105027":"POINT(77 21)", +"http://www.yso.fi/onto/yso/p105028":"POINT(26 60.5)", +"http://www.yso.fi/onto/yso/p105029":"POINT(19 47)", +"http://www.yso.fi/onto/yso/p105030":"POINT(25 46)", +"http://www.yso.fi/onto/yso/p105032":"POINT(-102.366667 23.316667)", +"http://www.yso.fi/onto/yso/p105034":"POINT(24.36997 61.79791)", +"http://www.yso.fi/onto/yso/p105035":"POINT(27.55563 61.82006)", +"http://www.yso.fi/onto/yso/p105037":"POINT(22 39)", +"http://www.yso.fi/onto/yso/p105038":"POINT(19.99113 59.8609)", +"http://www.yso.fi/onto/yso/p105040":"POINT(26.11098 61.74027)", +"http://www.yso.fi/onto/yso/p105054":"POINT(20.90837 59.92028)", +"http://www.yso.fi/onto/yso/p105060":"POINT(30.15422 62.8051)", +"http://www.yso.fi/onto/yso/p105061":"POINT(27.10806 61.77245)", +"http://www.yso.fi/onto/yso/p105065":"POINT(30.960833333333 61.374166666667)", +"http://www.yso.fi/onto/yso/p105067":"", +"http://www.yso.fi/onto/yso/p105068":"POINT(27.273611111111 61.6875)", +"http://www.yso.fi/onto/yso/p105069":"POINT(14.421388888889 50.088611111111)", +"http://www.yso.fi/onto/yso/p105070":"POINT(29.01542 62.72602)", +"http://www.yso.fi/onto/yso/p105071":"POINT(26.64247 61.98864)", +"http://www.yso.fi/onto/yso/p105072":"", +"http://www.yso.fi/onto/yso/p105073":"", +"http://www.yso.fi/onto/yso/p105076":"POINT(26.87536 61.41828)", +"http://www.yso.fi/onto/yso/p105078":"POINT(-100 40)", +"http://www.yso.fi/onto/yso/p105079":"POINT(27.26808 64.3091)", +"http://www.yso.fi/onto/yso/p105080":"POINT(30.26666667 62.86666667)", +"http://www.yso.fi/onto/yso/p105081":"POINT(12.48 41.89)", +"http://www.yso.fi/onto/yso/p105082":"POINT(29.69937 63.25098)", +"http://www.yso.fi/onto/yso/p105083":"POINT(30.06467 63.33687)", +"http://www.yso.fi/onto/yso/p105084":"", +"http://www.yso.fi/onto/yso/p105086":"POINT(25.11552 60.06123)", +"http://www.yso.fi/onto/yso/p105087":"POINT(10 51)", +"http://www.yso.fi/onto/yso/p105088":"POINT(12.389 52.052)", +"http://www.yso.fi/onto/yso/p105089":"POINT(7.0997222222222 50.733888888889)", +"http://www.yso.fi/onto/yso/p105092":"POINT(17 -23)", +"http://www.yso.fi/onto/yso/p105093":"POINT(29.46961 62.24092)", +"http://www.yso.fi/onto/yso/p105094":"", +"http://www.yso.fi/onto/yso/p105095":"POINT(27.678333333333 62.8925)", +"http://www.yso.fi/onto/yso/p105096":"POINT(27.22705 61.70099)", +"http://www.yso.fi/onto/yso/p105098":"POINT(24 -29)", +"http://www.yso.fi/onto/yso/p105101":"POINT(26.55814 60.48008)", +"http://www.yso.fi/onto/yso/p105102":"POINT(24.6678 61.36293)", +"http://www.yso.fi/onto/yso/p105103":"POINT(24.32651 61.4746)", +"http://www.yso.fi/onto/yso/p105104":"POINT(24.566666666667 61.505)", +"http://www.yso.fi/onto/yso/p105105":"POINT(146.1 -6.366667)", +"http://www.yso.fi/onto/yso/p105108":"", +"http://www.yso.fi/onto/yso/p105111":"POINT(12 43)", +"http://www.yso.fi/onto/yso/p105112":"POINT(24.47827 64.68773)", +"http://www.yso.fi/onto/yso/p105113":"POINT(140 -30)", +"http://www.yso.fi/onto/yso/p105115":"POINT(20.932 60.256)", +"http://www.yso.fi/onto/yso/p105116":"POINT(135 -25)", +"http://www.yso.fi/onto/yso/p105117":"POINT(24.441388888889 61)", +"http://www.yso.fi/onto/yso/p105118":"POINT(27.63167 61.57903)", +"http://www.yso.fi/onto/yso/p105119":"", +"http://www.yso.fi/onto/yso/p105120":"", +"http://www.yso.fi/onto/yso/p105121":"POINT(25.0565 60.28909)", +"http://www.yso.fi/onto/yso/p105122":"POINT(24.97511 60.42714)", +"http://www.yso.fi/onto/yso/p105123":"POINT(25.05697 60.44312)", +"http://www.yso.fi/onto/yso/p105124":"POINT(24.95982 60.26854)", +"http://www.yso.fi/onto/yso/p105125":"POINT(24.97958 60.21569)", +"http://www.yso.fi/onto/yso/p105126":"POINT(26.32998 64.09946)", +"http://www.yso.fi/onto/yso/p105129":"POINT(25.18813 65.57847)", +"http://www.yso.fi/onto/yso/p105130":"POINT(26.99924 65.36095)", +"http://www.yso.fi/onto/yso/p105131":"POINT(29.79717 62.60889)", +"http://www.yso.fi/onto/yso/p105132":"POINT(29.70779 62.52724)", +"http://www.yso.fi/onto/yso/p105133":"POINT(21.09933 63.43323)", +"http://www.yso.fi/onto/yso/p105136":"POINT(27.60171 68.95965)", +"http://www.yso.fi/onto/yso/p105137":"", +"http://www.yso.fi/onto/yso/p105138":"POINT(-40 72)", +"http://www.yso.fi/onto/yso/p105139":"", +"http://www.yso.fi/onto/yso/p105140":"POINT(22.95806 63.76094)", +"http://www.yso.fi/onto/yso/p105141":"POINT(23.70587 63.39826)", +"http://www.yso.fi/onto/yso/p105143":"POINT(23.55724 63.33838)", +"http://www.yso.fi/onto/yso/p105144":"POINT(22.93773 63.62581)", +"http://www.yso.fi/onto/yso/p105145":"", +"http://www.yso.fi/onto/yso/p105146":"POINT(29.3 59.6333)", +"http://www.yso.fi/onto/yso/p105147":"POINT(22.50307 61.33721)", +"http://www.yso.fi/onto/yso/p105148":"POINT(22.69944 61.17755)", +"http://www.yso.fi/onto/yso/p105149":"POINT(22.68964 61.28186)", +"http://www.yso.fi/onto/yso/p105150":"POINT(22.697222222222 61.284722222222)", +"http://www.yso.fi/onto/yso/p105151":"POINT(22.77544 61.32079)", +"http://www.yso.fi/onto/yso/p105152":"POINT(25.01947 61.07257)", +"http://www.yso.fi/onto/yso/p105154":"POINT(29.28229 61.91069)", +"http://www.yso.fi/onto/yso/p105155":"POINT(31.5 61)", +"http://www.yso.fi/onto/yso/p105158":"POINT(23.00366 61.51356)", +"http://www.yso.fi/onto/yso/p105160":"POINT(23.17781 62.20318)", +"http://www.yso.fi/onto/yso/p105162":"POINT(35 31.75)", +"http://www.yso.fi/onto/yso/p105167":"POINT(25.37498 65.32261)", +"http://www.yso.fi/onto/yso/p105169":"POINT(23.25031 60.45012)", +"http://www.yso.fi/onto/yso/p105171":"POINT(25.41195 64.80979)", +"http://www.yso.fi/onto/yso/p105172":"POINT(25.61586 64.65556)", +"http://www.yso.fi/onto/yso/p105173":"POINT(26.99547 64.18538)", +"http://www.yso.fi/onto/yso/p105176":"POINT(26.82442 65.40471)", +"http://www.yso.fi/onto/yso/p105177":"POINT(27.94849 65.58979)", +"http://www.yso.fi/onto/yso/p105179":"", +"http://www.yso.fi/onto/yso/p105181":"POINT(121 24)", +"http://www.yso.fi/onto/yso/p105182":"POINT(128 36)", +"http://www.yso.fi/onto/yso/p105184":"POINT(139.77 35.68)", +"http://www.yso.fi/onto/yso/p105188":"POINT(24.46754 60.98907)", +"http://www.yso.fi/onto/yso/p105189":"POINT(123.01666944444 11.333330555556)", +"http://www.yso.fi/onto/yso/p105190":"POINT(5.55 52.316666666667)", +"http://www.yso.fi/onto/yso/p105191":"", +"http://www.yso.fi/onto/yso/p105194":"POINT(35.921927777778 56.857827777778)", +"http://www.yso.fi/onto/yso/p105195":"POINT(-8 53)", +"http://www.yso.fi/onto/yso/p105196":"POINT(81 7)", +"http://www.yso.fi/onto/yso/p105197":"POINT(81 7)", +"http://www.yso.fi/onto/yso/p105198":"POINT(28.49071 64.67726)", +"http://www.yso.fi/onto/yso/p105199":"POINT(27.143888888889 60.575555555556)", +"http://www.yso.fi/onto/yso/p105200":"POINT(26.75761 62.71535)", +"http://www.yso.fi/onto/yso/p105201":"POINT(26.83542 62.62114)", +"http://www.yso.fi/onto/yso/p105202":"POINT(20.462222222222 44.820555555556)", +"http://www.yso.fi/onto/yso/p105204":"POINT(20.05912 60.27521)", +"http://www.yso.fi/onto/yso/p105206":"POINT(5 51)", +"http://www.yso.fi/onto/yso/p105207":"POINT(7.419744 43.731245)", +"http://www.yso.fi/onto/yso/p105209":"POINT(-3 40)", +"http://www.yso.fi/onto/yso/p105210":"POINT(21.579388888889 61.131388888889)", +"http://www.yso.fi/onto/yso/p105212":"POINT(26.80363 64.60468)", +"http://www.yso.fi/onto/yso/p105214":"POINT(26.90659 60.539)", +"http://www.yso.fi/onto/yso/p105217":"POINT(23.94356 62.81858)", +"http://www.yso.fi/onto/yso/p105218":"POINT(24.21017 62.87297)", +"http://www.yso.fi/onto/yso/p105219":"POINT(23.8206 63.16193)", +"http://www.yso.fi/onto/yso/p105220":"POINT(26.99452 60.5419)", +"http://www.yso.fi/onto/yso/p105221":"POINT(26.97979 60.46965)", +"http://www.yso.fi/onto/yso/p105222":"POINT(29.18996 62.17327)", +"http://www.yso.fi/onto/yso/p105223":"POINT(19 50)", +"http://www.yso.fi/onto/yso/p105224":"POINT(28.9169 62.08926)", +"http://www.yso.fi/onto/yso/p105225":"POINT(28.97222 62.0349)", +"http://www.yso.fi/onto/yso/p105226":"POINT(27.6597 63.07366)", +"http://www.yso.fi/onto/yso/p105228":"POINT(23.06524 63.10092)", +"http://www.yso.fi/onto/yso/p105231":"POINT(26.77737 61.64328)", +"http://www.yso.fi/onto/yso/p105232":"POINT(28.17391 66.10974)", +"http://www.yso.fi/onto/yso/p105234":"POINT(103.8 1.3)", +"http://www.yso.fi/onto/yso/p105235":"POINT(37.85 0.4)", +"http://www.yso.fi/onto/yso/p105236":"POINT(34.568611111111 49.574444444444)", +"http://www.yso.fi/onto/yso/p105237":"POINT(27.88859 62.31395)", +"http://www.yso.fi/onto/yso/p105238":"POINT(26.93125 60.52156)", +"http://www.yso.fi/onto/yso/p105240":"POINT(24.83598 60.25681)", +"http://www.yso.fi/onto/yso/p105241":"POINT(31.851666666667 61.369444444444)", +"http://www.yso.fi/onto/yso/p105242":"POINT(118 -2)", +"http://www.yso.fi/onto/yso/p105243":"POINT(101 15)", +"http://www.yso.fi/onto/yso/p105244":"POINT(19 52)", +"http://www.yso.fi/onto/yso/p105245":"POINT(25.77307 65.1297)", +"http://www.yso.fi/onto/yso/p105246":"POINT(25.4085 64.93396)", +"http://www.yso.fi/onto/yso/p105248":"POINT(23.59658 61.55716)", +"http://www.yso.fi/onto/yso/p105251":"POINT(104 47)", +"http://www.yso.fi/onto/yso/p105253":"POINT(22.57748 62.73354)", +"http://www.yso.fi/onto/yso/p105254":"POINT(4.3547 50.8467)", +"http://www.yso.fi/onto/yso/p105255":"POINT(10 53.55)", +"http://www.yso.fi/onto/yso/p105256":"POINT(11.966666666667 57.706944444444)", +"http://www.yso.fi/onto/yso/p105257":"POINT(23.75592 61.69874)", +"http://www.yso.fi/onto/yso/p105258":"POINT(21.72934 61.53691)", +"http://www.yso.fi/onto/yso/p105259":"POINT(22.65319 61.19713)", +"http://www.yso.fi/onto/yso/p105260":"POINT(23.76028 60.80813)", +"http://www.yso.fi/onto/yso/p105261":"", +"http://www.yso.fi/onto/yso/p105262":"POINT(23.75658 60.78917)", +"http://www.yso.fi/onto/yso/p105263":"POINT(24.56376 61.17279)", +"http://www.yso.fi/onto/yso/p105264":"POINT(22.12729 60.96766)", +"http://www.yso.fi/onto/yso/p105265":"POINT(29.763888888889 60.920833333333)", +"http://www.yso.fi/onto/yso/p105266":"POINT(27.73361 62.86323)", +"http://www.yso.fi/onto/yso/p105267":"POINT(26.46145 60.71989)", +"http://www.yso.fi/onto/yso/p105268":"POINT(25.76916667 62.14027778)", +"http://www.yso.fi/onto/yso/p105269":"POINT(-79.5 22)", +"http://www.yso.fi/onto/yso/p105270":"POINT(29.88141 62.59187)", +"http://www.yso.fi/onto/yso/p105274":"POINT(0 -90)", +"http://www.yso.fi/onto/yso/p105275":"POINT(89.866667 24.016667)", +"http://www.yso.fi/onto/yso/p105280":"POINT(53 32)", +"http://www.yso.fi/onto/yso/p105281":"POINT(29.70747 62.7768)", +"http://www.yso.fi/onto/yso/p105284":"POINT(27.69637 60.66922)", +"http://www.yso.fi/onto/yso/p105286":"POINT(25.46927 66.64788)", +"http://www.yso.fi/onto/yso/p105289":"POINT(22.79129 63.60123)", +"http://www.yso.fi/onto/yso/p105294":"POINT(14 48)", +"http://www.yso.fi/onto/yso/p105295":"POINT(8.231973 46.798562)", +"http://www.yso.fi/onto/yso/p105296":"POINT(36 68)", +"http://www.yso.fi/onto/yso/p105297":"POINT(33.083333333333 68.966666666667)", +"http://www.yso.fi/onto/yso/p105298":"POINT(21.66881 60.46578)", +"http://www.yso.fi/onto/yso/p105299":"POINT(24.17726 65.82516)", +"http://www.yso.fi/onto/yso/p105300":"POINT(30.666666666667 61.7)", +"http://www.yso.fi/onto/yso/p105301":"POINT(23.02989 63.50667)", +"http://www.yso.fi/onto/yso/p105302":"POINT(24.95682 60.1968)", +"http://www.yso.fi/onto/yso/p105303":"POINT(27.51288 67.10919)", +"http://www.yso.fi/onto/yso/p105304":"POINT(27.27876 67.15216)", +"http://www.yso.fi/onto/yso/p105306":"POINT(28.88851 62.16298)", +"http://www.yso.fi/onto/yso/p105307":"", +"http://www.yso.fi/onto/yso/p105308":"POINT(25.6042 66.58756)", +"http://www.yso.fi/onto/yso/p105309":"POINT(24.439443888889 65.800833055556)", +"http://www.yso.fi/onto/yso/p105310":"POINT(24.44051 65.79911)", +"http://www.yso.fi/onto/yso/p105312":"POINT(24.14594 65.91472)", +"http://www.yso.fi/onto/yso/p105313":"POINT(28.62943 62.42599)", +"http://www.yso.fi/onto/yso/p105314":"POINT(28.85503 62.47405)", +"http://www.yso.fi/onto/yso/p105316":"POINT(21.87087 60.47857)", +"http://www.yso.fi/onto/yso/p105317":"POINT(28.37804 61.78973)", +"http://www.yso.fi/onto/yso/p105321":"POINT(-49.9563 -0.5933)", +"http://www.yso.fi/onto/yso/p105323":"POINT(14.5 35.883333333333)", +"http://www.yso.fi/onto/yso/p105326":"POINT(25.3202 60.63608)", +"http://www.yso.fi/onto/yso/p105327":"POINT(25.42133 60.73415)", +"http://www.yso.fi/onto/yso/p105329":"POINT(24.30721 64.16665)", +"http://www.yso.fi/onto/yso/p105330":"POINT(39 8)", +"http://www.yso.fi/onto/yso/p105333":"POINT(24.40686 60.37092)", +"http://www.yso.fi/onto/yso/p105335":"", +"http://www.yso.fi/onto/yso/p105336":"POINT(72.2 26.9)", +"http://www.yso.fi/onto/yso/p105337":"POINT(105.1 12)", +"http://www.yso.fi/onto/yso/p105340":"POINT(24.50993 62.0218)", +"http://www.yso.fi/onto/yso/p105341":"POINT(21.62352 61.36131)", +"http://www.yso.fi/onto/yso/p105342":"POINT(22.17107 60.48608)", +"http://www.yso.fi/onto/yso/p105348":"POINT(28.61972222 60.75805556)", +"http://www.yso.fi/onto/yso/p105349":"POINT(24.95354 60.17301)", +"http://www.yso.fi/onto/yso/p105351":"POINT(24.54263 65.79898)", +"http://www.yso.fi/onto/yso/p105352":"POINT(24.544444444444 65.801388888889)", +"http://www.yso.fi/onto/yso/p105354":"POINT(30.316666666667 59.95)", +"http://www.yso.fi/onto/yso/p105356":"POINT(23.98153 60.47269)", +"http://www.yso.fi/onto/yso/p105357":"POINT(29.84441 62.76647)", +"http://www.yso.fi/onto/yso/p105358":"POINT(21.97304 60.99678)", +"http://www.yso.fi/onto/yso/p105359":"POINT(23.88538 60.39402)", +"http://www.yso.fi/onto/yso/p105360":"POINT(23.886111111111 60.391669444444)", +"http://www.yso.fi/onto/yso/p105361":"POINT(23.51769 60.62829)", +"http://www.yso.fi/onto/yso/p105364":"POINT(15.95 -17.9166666667)", +"http://www.yso.fi/onto/yso/p105366":"POINT(28.925 61.041666666667)", +"http://www.yso.fi/onto/yso/p105369":"POINT(25.31957 63.74851)", +"http://www.yso.fi/onto/yso/p105370":"", +"http://www.yso.fi/onto/yso/p105371":"POINT(24.93785 63.60439)", +"http://www.yso.fi/onto/yso/p105372":"POINT(25.65263 64.76336)", +"http://www.yso.fi/onto/yso/p105376":"POINT(23.06721 62.56922)", +"http://www.yso.fi/onto/yso/p105377":"POINT(-109 56)", +"http://www.yso.fi/onto/yso/p105378":"POINT(-78 -8)", +"http://www.yso.fi/onto/yso/p105379":"POINT(10 16)", +"http://www.yso.fi/onto/yso/p105384":"POINT(174 -42)", +"http://www.yso.fi/onto/yso/p105389":"POINT(25.27338 60.86838)", +"http://www.yso.fi/onto/yso/p105392":"POINT(24.106944444444 56.9475)", +"http://www.yso.fi/onto/yso/p105393":"POINT(25.90799 63.20518)", +"http://www.yso.fi/onto/yso/p105394":"POINT(22.95764 60.11387)", +"http://www.yso.fi/onto/yso/p105395":"POINT(24.24704 60.35474)", +"http://www.yso.fi/onto/yso/p105397":"POINT(21.48687 62.2454)", +"http://www.yso.fi/onto/yso/p105398":"POINT(0 90)", +"http://www.yso.fi/onto/yso/p105400":"POINT(-150 -70)", +"http://www.yso.fi/onto/yso/p105407":"POINT(25.66493 66.55715)", +"http://www.yso.fi/onto/yso/p105409":"POINT(-9.1833333333333 38.7)", +"http://www.yso.fi/onto/yso/p105410":"POINT(25.80638 62.77929)", +"http://www.yso.fi/onto/yso/p105416":"POINT(-100 48)", +"http://www.yso.fi/onto/yso/p105417":"POINT(21.85378 63.2165)", +"http://www.yso.fi/onto/yso/p105418":"POINT(20.42548 60.01129)", +"http://www.yso.fi/onto/yso/p105419":"POINT(26.0667 61.25)", +"http://www.yso.fi/onto/yso/p105423":"POINT(27.21762 67.0701)", +"http://www.yso.fi/onto/yso/p105424":"POINT(30.35 69.033055555556)", +"http://www.yso.fi/onto/yso/p105425":"POINT(25.59888 60.53021)", +"http://www.yso.fi/onto/yso/p105430":"POINT(23.445 63.02328)", +"http://www.yso.fi/onto/yso/p105431":"POINT(25.91446 63.58603)", +"http://www.yso.fi/onto/yso/p105437":"POINT(29.094722222222 61.098611111111)", +"http://www.yso.fi/onto/yso/p105440":"POINT(29.3211 67.79716)", +"http://www.yso.fi/onto/yso/p105442":"", +"http://www.yso.fi/onto/yso/p105445":"POINT(24.96885 60.71633)", +"http://www.yso.fi/onto/yso/p105446":"POINT(24.43443 59.98581)", +"http://www.yso.fi/onto/yso/p105447":"POINT(24.56644 63.04341)", +"http://www.yso.fi/onto/yso/p105448":"POINT(25.741666666667 62.288888888889)", +"http://www.yso.fi/onto/yso/p105449":"POINT(28.968055555556 60.375)", +"http://www.yso.fi/onto/yso/p105450":"", +"http://www.yso.fi/onto/yso/p105454":"POINT(25.42799 65.32465)", +"http://www.yso.fi/onto/yso/p105458":"POINT(23.54282 60.12992)", +"http://www.yso.fi/onto/yso/p105459":"POINT(23.50639 62.80741)", +"http://www.yso.fi/onto/yso/p105460":"POINT(24.97132 60.16588)", +"http://www.yso.fi/onto/yso/p105462":"POINT(27.1494 66.3433)", +"http://www.yso.fi/onto/yso/p105463":"POINT(21.50543 61.86034)", +"http://www.yso.fi/onto/yso/p105464":"POINT(18.068611111111 59.329444444444)", +"http://www.yso.fi/onto/yso/p105466":"POINT(24.57507 64.69342)", +"http://www.yso.fi/onto/yso/p105467":"POINT(24.99155 64.48472)", +"http://www.yso.fi/onto/yso/p105469":"POINT(27.3928 68.78075)", +"http://www.yso.fi/onto/yso/p105471":"POINT(13.271484 59.425522)", +"http://www.yso.fi/onto/yso/p105476":"POINT(24.133333333333 65.833333333333)", +"http://www.yso.fi/onto/yso/p105477":"POINT(25.2395 63.53475)", +"http://www.yso.fi/onto/yso/p105478":"POINT(25.10329 64.66384)", +"http://www.yso.fi/onto/yso/p105479":"POINT(23.96348 66.77558)", +"http://www.yso.fi/onto/yso/p105480":"POINT(30.16893 62.93711)", +"http://www.yso.fi/onto/yso/p105484":"POINT(114.15861111111 22.278333333333)", +"http://www.yso.fi/onto/yso/p105485":"POINT(22.34974 60.53711)", +"http://www.yso.fi/onto/yso/p105490":"POINT(-90.25 15.5)", +"http://www.yso.fi/onto/yso/p105491":"POINT(24.86694444 60.25111111)", +"http://www.yso.fi/onto/yso/p105492":"POINT(25.14794 60.21866)", +"http://www.yso.fi/onto/yso/p105493":"POINT(27.20266 66.28884)", +"http://www.yso.fi/onto/yso/p105494":"POINT(26.27921 64.35002)", +"http://www.yso.fi/onto/yso/p105495":"POINT(27.66907 64.86866)", +"http://www.yso.fi/onto/yso/p105496":"POINT(25.9333 63.16958)", +"http://www.yso.fi/onto/yso/p105498":"POINT(25.06496 60.24277)", +"http://www.yso.fi/onto/yso/p105499":"POINT(30.27042 62.91084)", +"http://www.yso.fi/onto/yso/p105500":"POINT(35.145264 31.625321)", +"http://www.yso.fi/onto/yso/p105501":"", +"http://www.yso.fi/onto/yso/p105502":"POINT(26.13148 61.15745)", +"http://www.yso.fi/onto/yso/p105503":"POINT(23.03353 61.96707)", +"http://www.yso.fi/onto/yso/p105504":"POINT(29.62778 66.19816)", +"http://www.yso.fi/onto/yso/p105507":"POINT(22.32618 61.13755)", +"http://www.yso.fi/onto/yso/p105508":"POINT(30.33306 62.22624)", +"http://www.yso.fi/onto/yso/p105510":"POINT(-88.866111 13.668889)", +"http://www.yso.fi/onto/yso/p105511":"POINT(-86.000988888889 13.091380555556)", +"http://www.yso.fi/onto/yso/p105512":"POINT(22.45593 60.50688)", +"http://www.yso.fi/onto/yso/p105513":"POINT(26.86636 63.59392)", +"http://www.yso.fi/onto/yso/p105514":"POINT(28.12642 62.49384)", +"http://www.yso.fi/onto/yso/p105515":"POINT(27.32553 62.11956)", +"http://www.yso.fi/onto/yso/p105517":"POINT(23.91505 63.90175)", +"http://www.yso.fi/onto/yso/p105521":"POINT(22.951874 63.517035)", +"http://www.yso.fi/onto/yso/p105522":"POINT(23.03761 63.59884)", +"http://www.yso.fi/onto/yso/p105524":"POINT(28.81499896 60.7865583)", +"http://www.yso.fi/onto/yso/p105525":"POINT(24.14098 65.8759)", +"http://www.yso.fi/onto/yso/p105526":"POINT(23.5815 67.21362)", +"http://www.yso.fi/onto/yso/p105527":"POINT(27.15735 62.45714)", +"http://www.yso.fi/onto/yso/p105528":"POINT(46 23)", +"http://www.yso.fi/onto/yso/p105529":"POINT(21.82314 61.87766)", +"http://www.yso.fi/onto/yso/p105530":"POINT(21.70028 61.02929)", +"http://www.yso.fi/onto/yso/p105531":"POINT(24.15096 63.91505)", +"http://www.yso.fi/onto/yso/p105536":"POINT(22.30967 61.11562)", +"http://www.yso.fi/onto/yso/p105539":"POINT(24.78933 60.20049)", +"http://www.yso.fi/onto/yso/p105540":"POINT(25.37293 60.4769)", +"http://www.yso.fi/onto/yso/p105542":"POINT(23.27244 60.80523)", +"http://www.yso.fi/onto/yso/p105543":"", +"http://www.yso.fi/onto/yso/p105544":"POINT(30.043316 63.311295)", +"http://www.yso.fi/onto/yso/p105545":"POINT(27.68602 61.19874)", +"http://www.yso.fi/onto/yso/p105546":"POINT(28.8158 61.29226)", +"http://www.yso.fi/onto/yso/p105547":"POINT(24.39409 60.09162)", +"http://www.yso.fi/onto/yso/p105548":"", +"http://www.yso.fi/onto/yso/p105550":"POINT(26 67)", +"http://www.yso.fi/onto/yso/p105551":"POINT(23.91747 67.49571)", +"http://www.yso.fi/onto/yso/p105552":"POINT(-73.300833 16.256944)", +"http://www.yso.fi/onto/yso/p105553":"POINT(12.482777777778 41.893055555556)", +"http://www.yso.fi/onto/yso/p105554":"POINT(19.040833333333 47.498333333333)", +"http://www.yso.fi/onto/yso/p105555":"POINT(-73.555 45.508056)", +"http://www.yso.fi/onto/yso/p105565":"POINT(47.3 8.316667)", +"http://www.yso.fi/onto/yso/p105570":"POINT(2.9833333333333 39.616666666667)", +"http://www.yso.fi/onto/yso/p105571":"POINT(1.43 38.98)", +"http://www.yso.fi/onto/yso/p105572":"POINT(4.0833333333333 39.966666666667)", +"http://www.yso.fi/onto/yso/p105573":"POINT(36 39)", +"http://www.yso.fi/onto/yso/p105574":"POINT(9 34)", +"http://www.yso.fi/onto/yso/p105575":"POINT(-81.631666666667 28.133333333333)", +"http://www.yso.fi/onto/yso/p105576":"POINT(24.91399 60.16333)", +"http://www.yso.fi/onto/yso/p105579":"", +"http://www.yso.fi/onto/yso/p105581":"POINT(28.87685 61.23731)", +"http://www.yso.fi/onto/yso/p105582":"POINT(27.83216 64.40945)", +"http://www.yso.fi/onto/yso/p105583":"POINT(23.7382 60.5715)", +"http://www.yso.fi/onto/yso/p105585":"POINT(24.80539 62.66504)", +"http://www.yso.fi/onto/yso/p105586":"POINT(-54.883333333333 -2.3)", +"http://www.yso.fi/onto/yso/p105587":"", +"http://www.yso.fi/onto/yso/p105588":"POINT(24.1319 62.15631)", +"http://www.yso.fi/onto/yso/p105589":"POINT(27.01983 69.75804)", +"http://www.yso.fi/onto/yso/p105590":"POINT(24.0154 62.18269)", +"http://www.yso.fi/onto/yso/p105592":"POINT(25.27463 66.17093)", +"http://www.yso.fi/onto/yso/p105593":"POINT(23.1275 63.22872)", +"http://www.yso.fi/onto/yso/p105594":"POINT(30.25082 62.49525)", +"http://www.yso.fi/onto/yso/p105597":"POINT(23.65184 60.93775)", +"http://www.yso.fi/onto/yso/p105598":"POINT(-75.5 39)", +"http://www.yso.fi/onto/yso/p105603":"POINT(24.12177 61.10081)", +"http://www.yso.fi/onto/yso/p105606":"POINT(24.9437 60.1842)", +"http://www.yso.fi/onto/yso/p105607":"POINT(24.96597 60.18581)", +"http://www.yso.fi/onto/yso/p105608":"POINT(25.07696 60.25742)", +"http://www.yso.fi/onto/yso/p105609":"POINT(25.03086 60.17521)", +"http://www.yso.fi/onto/yso/p105610":"POINT(25.03077 60.19417)", +"http://www.yso.fi/onto/yso/p105611":"POINT(2.1769444444444 41.3825)", +"http://www.yso.fi/onto/yso/p105612":"POINT(13.383333333333 52.516666666667)", +"http://www.yso.fi/onto/yso/p105613":"POINT(27.44782 61.32852)", +"http://www.yso.fi/onto/yso/p105614":"POINT(27.89516 62.99591)", +"http://www.yso.fi/onto/yso/p105617":"POINT(23.01646 62.93643)", +"http://www.yso.fi/onto/yso/p105618":"POINT(22.5072 63.56036)", +"http://www.yso.fi/onto/yso/p105619":"POINT(30.5167 60.5667)", +"http://www.yso.fi/onto/yso/p105620":"", +"http://www.yso.fi/onto/yso/p105623":"POINT(25.50995 64.91267)", +"http://www.yso.fi/onto/yso/p105630":"POINT(25.57461 63.37033)", +"http://www.yso.fi/onto/yso/p105632":"POINT(143 51)", +"http://www.yso.fi/onto/yso/p105633":"", +"http://www.yso.fi/onto/yso/p105636":"POINT(30.14539 62.09669)", +"http://www.yso.fi/onto/yso/p105637":"POINT(30.01283 62.0839)", +"http://www.yso.fi/onto/yso/p105638":"POINT(24.79166 60.16316)", +"http://www.yso.fi/onto/yso/p105639":"POINT(27.33795 63.66146)", +"http://www.yso.fi/onto/yso/p105641":"POINT(27.60744 63.09882)", +"http://www.yso.fi/onto/yso/p105642":"", +"http://www.yso.fi/onto/yso/p105643":"", +"http://www.yso.fi/onto/yso/p105645":"POINT(21.48324 61.23609)", +"http://www.yso.fi/onto/yso/p105646":"POINT(28.26473 61.07056)", +"http://www.yso.fi/onto/yso/p105647":"POINT(23.75692 60.48017)", +"http://www.yso.fi/onto/yso/p105648":"POINT(19.5562 60.21985)", +"http://www.yso.fi/onto/yso/p105649":"POINT(19.61314 60.21361)", +"http://www.yso.fi/onto/yso/p105656":"POINT(25.56356 65.00632)", +"http://www.yso.fi/onto/yso/p105657":"POINT(27.49725 67.16324)", +"http://www.yso.fi/onto/yso/p105658":"POINT(25.60011 60.47886)", +"http://www.yso.fi/onto/yso/p105659":"POINT(13.25 59.75)", +"http://www.yso.fi/onto/yso/p105663":"POINT(-73.94 40.67)", +"http://www.yso.fi/onto/yso/p105667":"POINT(24.00514 60.28697)", +"http://www.yso.fi/onto/yso/p105673":"POINT(52 30)", +"http://www.yso.fi/onto/yso/p105675":"POINT(22.31101 63.41726)", +"http://www.yso.fi/onto/yso/p105676":"POINT(23.043333333333 69.011666666667)", +"http://www.yso.fi/onto/yso/p105678":"POINT(24.96996 63.36727)", +"http://www.yso.fi/onto/yso/p105679":"", +"http://www.yso.fi/onto/yso/p105680":"POINT(25.283333333333 54.683333333333)", +"http://www.yso.fi/onto/yso/p105681":"POINT(-82.383333333333 23.116666666667)", +"http://www.yso.fi/onto/yso/p105682":"POINT(-75.820555555556 20.028333333333)", +"http://www.yso.fi/onto/yso/p105683":"POINT(21.033333333333 52.216666666667)", +"http://www.yso.fi/onto/yso/p105684":"POINT(19.95 50.066666666667)", +"http://www.yso.fi/onto/yso/p105685":"POINT(30.216666666667 51.266666666667)", +"http://www.yso.fi/onto/yso/p105686":"POINT(28.31899 60.86571)", +"http://www.yso.fi/onto/yso/p105687":"POINT(28.31899 60.86571)", +"http://www.yso.fi/onto/yso/p105688":"POINT(21.86958 61.59373)", +"http://www.yso.fi/onto/yso/p105689":"POINT(28.90479 64.88382)", +"http://www.yso.fi/onto/yso/p105690":"POINT(44.116667 23.716667)", +"http://www.yso.fi/onto/yso/p105691":"POINT(25.07097 63.12292)", +"http://www.yso.fi/onto/yso/p105692":"POINT(25.12671 60.23453)", +"http://www.yso.fi/onto/yso/p105693":"POINT(26.47479 60.52439)", +"http://www.yso.fi/onto/yso/p105694":"POINT(23.61221 62.25691)", +"http://www.yso.fi/onto/yso/p105695":"POINT(24.51821 67.68171)", +"http://www.yso.fi/onto/yso/p105702":"POINT(23.67947 64.04196)", +"http://www.yso.fi/onto/yso/p105704":"POINT(23.04288 61.43866)", +"http://www.yso.fi/onto/yso/p105705":"POINT(105.666667 12.733333)", +"http://www.yso.fi/onto/yso/p105706":"POINT(1.5552777777778 42.558333333333)", +"http://www.yso.fi/onto/yso/p105707":"POINT(9.553889 47.145)", +"http://www.yso.fi/onto/yso/p105708":"POINT(6.13 49.77)", +"http://www.yso.fi/onto/yso/p105709":"POINT(12.466666666667 43.933333333333)", +"http://www.yso.fi/onto/yso/p105710":"POINT(12.453 41.904)", +"http://www.yso.fi/onto/yso/p105712":"POINT(28.47918 63.20623)", +"http://www.yso.fi/onto/yso/p105713":"POINT(26.30753 61.84221)", +"http://www.yso.fi/onto/yso/p105714":"POINT(114.566667 4.4)", +"http://www.yso.fi/onto/yso/p105715":"POINT(96 22)", +"http://www.yso.fi/onto/yso/p105716":"POINT(102.566667 19.366667)", +"http://www.yso.fi/onto/yso/p105717":"POINT(111.55 2.316667)", +"http://www.yso.fi/onto/yso/p105720":"POINT(-5.84 54.590933333333)", +"http://www.yso.fi/onto/yso/p105721":"POINT(22.21472 61.73784)", +"http://www.yso.fi/onto/yso/p105725":"POINT(29.3696 62.85683)", +"http://www.yso.fi/onto/yso/p105726":"POINT(25.57574 65.04271)", +"http://www.yso.fi/onto/yso/p105727":"POINT(22.73321 60.58397)", +"http://www.yso.fi/onto/yso/p105728":"POINT(21.9219 60.6266)", +"http://www.yso.fi/onto/yso/p105729":"POINT(23.49009 60.80516)", +"http://www.yso.fi/onto/yso/p105731":"POINT(29.1333 60.95)", +"http://www.yso.fi/onto/yso/p105732":"POINT(26.79825 60.68436)", +"http://www.yso.fi/onto/yso/p105733":"POINT(27.0056 60.7364)", +"http://www.yso.fi/onto/yso/p105740":"POINT(25.08274 60.2018)", +"http://www.yso.fi/onto/yso/p105741":"POINT(25.01782 60.39949)", +"http://www.yso.fi/onto/yso/p105742":"POINT(23.85747 61.45432)", +"http://www.yso.fi/onto/yso/p105743":"POINT(23.848 61.49623)", +"http://www.yso.fi/onto/yso/p105744":"POINT(26.62249 63.65146)", +"http://www.yso.fi/onto/yso/p105746":"POINT(25.01363 60.23717)", +"http://www.yso.fi/onto/yso/p105747":"POINT(24.80634 60.17653)", +"http://www.yso.fi/onto/yso/p105748":"POINT(29.8154 63.09612)", +"http://www.yso.fi/onto/yso/p105749":"POINT(30.62732 62.48372)", +"http://www.yso.fi/onto/yso/p105750":"POINT(25.67357 62.13051)", +"http://www.yso.fi/onto/yso/p105751":"", +"http://www.yso.fi/onto/yso/p105754":"POINT(25.107 61.18933)", +"http://www.yso.fi/onto/yso/p105758":"POINT(29.00548 62.46666)", +"http://www.yso.fi/onto/yso/p105759":"POINT(21.64417 60.9912)", +"http://www.yso.fi/onto/yso/p105760":"POINT(24.39085 61.07197)", +"http://www.yso.fi/onto/yso/p105763":"POINT(21.86614 60.60445)", +"http://www.yso.fi/onto/yso/p105764":"POINT(21.4169 63.22882)", +"http://www.yso.fi/onto/yso/p105766":"POINT(21.96637 59.86636)", +"http://www.yso.fi/onto/yso/p105768":"POINT(28.50852 61.11818)", +"http://www.yso.fi/onto/yso/p105770":"POINT(22.09869 60.57099)", +"http://www.yso.fi/onto/yso/p105777":"POINT(26.51842 65.92751)", +"http://www.yso.fi/onto/yso/p105778":"POINT(24.01536 64.12978)", +"http://www.yso.fi/onto/yso/p105780":"POINT(29.7 60.2)", +"http://www.yso.fi/onto/yso/p105791":"POINT(24.93351 60.19883)", +"http://www.yso.fi/onto/yso/p105792":"POINT(25.02386 60.22182)", +"http://www.yso.fi/onto/yso/p105793":"POINT(29.65792 64.81354)", +"http://www.yso.fi/onto/yso/p105794":"POINT(24.6658 63.5248)", +"http://www.yso.fi/onto/yso/p105795":"POINT(24.2505 63.77169)", +"http://www.yso.fi/onto/yso/p105796":"POINT(23.42005 59.99121)", +"http://www.yso.fi/onto/yso/p105799":"POINT(24.45847 64.64923)", +"http://www.yso.fi/onto/yso/p105800":"POINT(26.41285 62.93103)", +"http://www.yso.fi/onto/yso/p105801":"POINT(24.79692 60.23779)", +"http://www.yso.fi/onto/yso/p105802":"POINT(24.8002 60.21687)", +"http://www.yso.fi/onto/yso/p105803":"POINT(28.8497 62.56773)", +"http://www.yso.fi/onto/yso/p105804":"POINT(22.33679 61.04836)", +"http://www.yso.fi/onto/yso/p105805":"POINT(25.58041 60.64535)", +"http://www.yso.fi/onto/yso/p105806":"POINT(25.66619 60.65885)", +"http://www.yso.fi/onto/yso/p105807":"POINT(29.52954 61.88533)", +"http://www.yso.fi/onto/yso/p105808":"POINT(28.68844 62.45162)", +"http://www.yso.fi/onto/yso/p105810":"POINT(23.65447 64.06218)", +"http://www.yso.fi/onto/yso/p105812":"POINT(25.94128 60.49111)", +"http://www.yso.fi/onto/yso/p105815":"POINT(-4 17)", +"http://www.yso.fi/onto/yso/p105816":"POINT(23.676 67.058)", +"http://www.yso.fi/onto/yso/p105818":"POINT(-1.2689 60.3038)", +"http://www.yso.fi/onto/yso/p105819":"POINT(-6.8444805555556 61.969944444444)", +"http://www.yso.fi/onto/yso/p105820":"POINT(-17.986666666667 64.830555555556)", +"http://www.yso.fi/onto/yso/p105825":"POINT(19.29387 60.14072)", +"http://www.yso.fi/onto/yso/p105826":"POINT(24.44027 60.6839)", +"http://www.yso.fi/onto/yso/p105827":"POINT(24.48629 60.73006)", +"http://www.yso.fi/onto/yso/p105828":"POINT(24.51614 60.75156)", +"http://www.yso.fi/onto/yso/p105829":"POINT(23.47446 61.4084)", +"http://www.yso.fi/onto/yso/p105830":"POINT(23.19406 61.44919)", +"http://www.yso.fi/onto/yso/p105831":"POINT(22.96977 61.3524)", +"http://www.yso.fi/onto/yso/p105832":"POINT(22.85928 61.34547)", +"http://www.yso.fi/onto/yso/p105835":"POINT(-120 37)", +"http://www.yso.fi/onto/yso/p105836":"POINT(25.43852 62.50081)", +"http://www.yso.fi/onto/yso/p105839":"POINT(-5 57)", +"http://www.yso.fi/onto/yso/p105840":"POINT(-6 32)", +"http://www.yso.fi/onto/yso/p105841":"POINT(29 26)", +"http://www.yso.fi/onto/yso/p105846":"POINT(23.39743 61.23699)", +"http://www.yso.fi/onto/yso/p105847":"POINT(14.416666666667 50.083333333333)", +"http://www.yso.fi/onto/yso/p105848":"POINT(20.94899 63.40962)", +"http://www.yso.fi/onto/yso/p105850":"POINT(15.4 -4.4)", +"http://www.yso.fi/onto/yso/p105851":"POINT(28.666666666667 66.833333333333)", +"http://www.yso.fi/onto/yso/p105856":"", +"http://www.yso.fi/onto/yso/p105857":"POINT(28.51656 62.49663)", +"http://www.yso.fi/onto/yso/p105858":"POINT(28.39951 62.5032)", +"http://www.yso.fi/onto/yso/p105859":"POINT(28.50916 62.53487)", +"http://www.yso.fi/onto/yso/p105861":"POINT(23.29838 60.06459)", +"http://www.yso.fi/onto/yso/p105862":"POINT(26.43619 62.3903)", +"http://www.yso.fi/onto/yso/p105863":"POINT(25.4 36.4)", +"http://www.yso.fi/onto/yso/p105865":"POINT(21.18873 62.78505)", +"http://www.yso.fi/onto/yso/p105866":"POINT(-150 64)", +"http://www.yso.fi/onto/yso/p105873":"POINT(16 78)", +"http://www.yso.fi/onto/yso/p105874":"POINT(24.8 61.729166666667)", +"http://www.yso.fi/onto/yso/p105884":"POINT(28.25 -29.55)", +"http://www.yso.fi/onto/yso/p105885":"POINT(30.016667 -19.016667)", +"http://www.yso.fi/onto/yso/p105886":"POINT(20 67)", +"http://www.yso.fi/onto/yso/p105888":"POINT(25.85363 62.56422)", +"http://www.yso.fi/onto/yso/p105890":"POINT(31.28402 62.93194)", +"http://www.yso.fi/onto/yso/p105891":"POINT(24.44185 64.29772)", +"http://www.yso.fi/onto/yso/p105896":"POINT(28.15402 62.91277)", +"http://www.yso.fi/onto/yso/p105899":"POINT(43 37)", +"http://www.yso.fi/onto/yso/p105902":"POINT(21.31523 60.88676)", +"http://www.yso.fi/onto/yso/p105903":"POINT(21.294 60.85266)", +"http://www.yso.fi/onto/yso/p105906":"", +"http://www.yso.fi/onto/yso/p105909":"POINT(27.06666667 62.28333333)", +"http://www.yso.fi/onto/yso/p105912":"POINT(25.54914 61.17251)", +"http://www.yso.fi/onto/yso/p105913":"POINT(25.66773 61.13271)", +"http://www.yso.fi/onto/yso/p105916":"POINT(23.12636 63.81345)", +"http://www.yso.fi/onto/yso/p105917":"", +"http://www.yso.fi/onto/yso/p105917":"POINT(26 60.5)", +"http://www.yso.fi/onto/yso/p105918":"POINT(-16.25 28.466666666667)", +"http://www.yso.fi/onto/yso/p105919":"POINT(-16.605555555556 28.268611111111)", +"http://www.yso.fi/onto/yso/p105920":"POINT(25.1135 60.42587)", +"http://www.yso.fi/onto/yso/p105921":"POINT(30.11619 62.93477)", +"http://www.yso.fi/onto/yso/p105924":"POINT(23.33854 63.70731)", +"http://www.yso.fi/onto/yso/p105925":"POINT(24.97367 60.24458)", +"http://www.yso.fi/onto/yso/p105926":"POINT(5.3763888888889 43.296666666667)", +"http://www.yso.fi/onto/yso/p105928":"POINT(25.93008 60.94774)", +"http://www.yso.fi/onto/yso/p105945":"", +"http://www.yso.fi/onto/yso/p105946":"", +"http://www.yso.fi/onto/yso/p105948":"POINT(27.85479 62.28903)", +"http://www.yso.fi/onto/yso/p105952":"POINT(29.92676 62.37035)", +"http://www.yso.fi/onto/yso/p105953":"POINT(30.01005 62.32592)", +"http://www.yso.fi/onto/yso/p105955":"POINT(30.70026 62.96226)", +"http://www.yso.fi/onto/yso/p105956":"POINT(27.86915 62.30224)", +"http://www.yso.fi/onto/yso/p105957":"POINT(24.65872 63.80431)", +"http://www.yso.fi/onto/yso/p105963":"POINT(23.86335 63.09238)", +"http://www.yso.fi/onto/yso/p105964":"POINT(24.9515 60.17927)", +"http://www.yso.fi/onto/yso/p105965":"POINT(69 29)", +"http://www.yso.fi/onto/yso/p105967":"POINT(24.42048 65.95894)", +"http://www.yso.fi/onto/yso/p105968":"POINT(24.42048 65.95894)", +"http://www.yso.fi/onto/yso/p105976":"POINT(27.0176 68.90547)", +"http://www.yso.fi/onto/yso/p105977":"POINT(24.02531 66.04044)", +"http://www.yso.fi/onto/yso/p105980":"POINT(28.82769 61.87891)", +"http://www.yso.fi/onto/yso/p105982":"POINT(24.40368 66.027)", +"http://www.yso.fi/onto/yso/p105983":"POINT(24.30828 65.98891)", +"http://www.yso.fi/onto/yso/p105984":"POINT(24.47058 65.99549)", +"http://www.yso.fi/onto/yso/p105985":"POINT(90.5 27.45)", +"http://www.yso.fi/onto/yso/p105988":"POINT(10.686388888889 53.869722222222)", +"http://www.yso.fi/onto/yso/p105991":"POINT(23.55304 62.45201)", +"http://www.yso.fi/onto/yso/p105992":"POINT(21.2667 62.86)", +"http://www.yso.fi/onto/yso/p105993":"POINT(22.92777 60.78)", +"http://www.yso.fi/onto/yso/p105994":"POINT(23.15201 60.93973)", +"http://www.yso.fi/onto/yso/p105995":"POINT(22.98333333 60.86666667)", +"http://www.yso.fi/onto/yso/p105996":"POINT(22.98333333 60.86666667)", +"http://www.yso.fi/onto/yso/p105997":"POINT(25.3426 65.18445)", +"http://www.yso.fi/onto/yso/p105998":"POINT(24.2242 67.56464)", +"http://www.yso.fi/onto/yso/p105999":"POINT(39.316666666667 -6.1333333333333)", +"http://www.yso.fi/onto/yso/p106002":"POINT(32.966666666667 60.983333333333)", +"http://www.yso.fi/onto/yso/p106003":"", +"http://www.yso.fi/onto/yso/p106013":"POINT(35.183333 -17.6)", +"http://www.yso.fi/onto/yso/p106014":"POINT(33.6 -13.016667)", +"http://www.yso.fi/onto/yso/p106015":"POINT(-1 8)", +"http://www.yso.fi/onto/yso/p106016":"POINT(30.155277777778 61.438333333333)", +"http://www.yso.fi/onto/yso/p106017":"POINT(-96 19)", +"http://www.yso.fi/onto/yso/p106018":"POINT(29.71442 63.40195)", +"http://www.yso.fi/onto/yso/p106020":"POINT(25.67059 61.29723)", +"http://www.yso.fi/onto/yso/p106021":"POINT(26.79674 61.09009)", +"http://www.yso.fi/onto/yso/p106022":"POINT(-7.9352777777778 37.014444444444)", +"http://www.yso.fi/onto/yso/p106027":"", +"http://www.yso.fi/onto/yso/p106035":"POINT(19.84577 60.37427)", +"http://www.yso.fi/onto/yso/p106036":"POINT(27.6753 62.88131)", +"http://www.yso.fi/onto/yso/p106037":"POINT(21.73878 63.03006)", +"http://www.yso.fi/onto/yso/p106038":"POINT(12.331944444444 45.439722222222)", +"http://www.yso.fi/onto/yso/p106039":"POINT(33.35 35.166666666667)", +"http://www.yso.fi/onto/yso/p106040":"POINT(26.82023 65.28051)", +"http://www.yso.fi/onto/yso/p106041":"POINT(25.6161 62.16341)", +"http://www.yso.fi/onto/yso/p106044":"POINT(32.39 1.28)", +"http://www.yso.fi/onto/yso/p106045":"POINT(25.62137 60.99658)", +"http://www.yso.fi/onto/yso/p106064":"POINT(21.43385 62.83646)", +"http://www.yso.fi/onto/yso/p106067":"POINT(26.052780555556 60.572219444444)", +"http://www.yso.fi/onto/yso/p106068":"POINT(23.34636 60.88958)", +"http://www.yso.fi/onto/yso/p106069":"POINT(21.19505 62.51117)", +"http://www.yso.fi/onto/yso/p106070":"POINT(23.69988 63.54809)", +"http://www.yso.fi/onto/yso/p106071":"POINT(23.47639 60.24434)", +"http://www.yso.fi/onto/yso/p106072":"POINT(23.44749 60.23646)", +"http://www.yso.fi/onto/yso/p106073":"POINT(27.38226 60.46243)", +"http://www.yso.fi/onto/yso/p106097":"POINT(29.133 63.54427)", +"http://www.yso.fi/onto/yso/p106111":"POINT(21.36049 60.22701)", +"http://www.yso.fi/onto/yso/p106112":"POINT(28.8786 61.8675)", +"http://www.yso.fi/onto/yso/p106114":"POINT(22.29892 60.98893)", +"http://www.yso.fi/onto/yso/p106116":"POINT(25.71642 62.21788)", +"http://www.yso.fi/onto/yso/p106117":"POINT(26.65597 61.96473)", +"http://www.yso.fi/onto/yso/p106119":"POINT(14 37.5)", +"http://www.yso.fi/onto/yso/p106121":"POINT(27.8481 63.14587)", +"http://www.yso.fi/onto/yso/p106139":"POINT(23.87396 60.04871)", +"http://www.yso.fi/onto/yso/p106141":"POINT(25.15 61.025)", +"http://www.yso.fi/onto/yso/p106142":"", +"http://www.yso.fi/onto/yso/p106143":"POINT(21.844444444444 61.101388888889)", +"http://www.yso.fi/onto/yso/p106144":"POINT(32 39)", +"http://www.yso.fi/onto/yso/p106165":"", +"http://www.yso.fi/onto/yso/p106166":"POINT(26.28946 62.62733)", +"http://www.yso.fi/onto/yso/p106167":"POINT(26.86603 68.87065)", +"http://www.yso.fi/onto/yso/p106176":"POINT(91.1 29.65)", +"http://www.yso.fi/onto/yso/p106177":"POINT(22.51817 61.95284)", +"http://www.yso.fi/onto/yso/p106178":"POINT(22.50412 61.91233)", +"http://www.yso.fi/onto/yso/p106202":"POINT(66 34)", +"http://www.yso.fi/onto/yso/p106204":"POINT(22.81948 63.28658)", +"http://www.yso.fi/onto/yso/p106222":"POINT(24.58866 65.84934)", +"http://www.yso.fi/onto/yso/p106245":"POINT(24.48498 60.18416)", +"http://www.yso.fi/onto/yso/p106246":"POINT(24.79462 62.4087)", +"http://www.yso.fi/onto/yso/p106248":"POINT(22.31821 63.0001)", +"http://www.yso.fi/onto/yso/p106251":"POINT(24.48719 61.91387)", +"http://www.yso.fi/onto/yso/p106252":"POINT(25.08317 60.22012)", +"http://www.yso.fi/onto/yso/p106253":"POINT(23.50354 61.19236)", +"http://www.yso.fi/onto/yso/p106255":"POINT(23.54931 61.08113)", +"http://www.yso.fi/onto/yso/p106257":"POINT(-91 17.4)", +"http://www.yso.fi/onto/yso/p106260":"POINT(64.066667 41.666667)", +"http://www.yso.fi/onto/yso/p106262":"POINT(21.37186 60.95282)", +"http://www.yso.fi/onto/yso/p106264":"POINT(43.733330555556 42.016669444444)", +"http://www.yso.fi/onto/yso/p106265":"POINT(23.26621 63.82647)", +"http://www.yso.fi/onto/yso/p106266":"POINT(27.77021 62.94358)", +"http://www.yso.fi/onto/yso/p106267":"POINT(20.85521 69.04377)", +"http://www.yso.fi/onto/yso/p106268":"POINT(25.916666666667 -24.666666666667)", +"http://www.yso.fi/onto/yso/p106270":"POINT(23.61094 60.09461)", +"http://www.yso.fi/onto/yso/p106271":"POINT(23.49894 60.04554)", +"http://www.yso.fi/onto/yso/p106272":"POINT(23.65738 61.52668)", +"http://www.yso.fi/onto/yso/p106273":"POINT(23.67061 61.52696)", +"http://www.yso.fi/onto/yso/p106274":"POINT(23.57965 61.49608)", +"http://www.yso.fi/onto/yso/p106275":"POINT(-3 48)", +"http://www.yso.fi/onto/yso/p106276":"POINT(0.01666667 49.2)", +"http://www.yso.fi/onto/yso/p106277":"POINT(43 33)", +"http://www.yso.fi/onto/yso/p106280":"POINT(17.5 65)", +"http://www.yso.fi/onto/yso/p106286":"POINT(25.37502 66.37511)", +"http://www.yso.fi/onto/yso/p106287":"POINT(27.79416 65.14481)", +"http://www.yso.fi/onto/yso/p106289":"POINT(24.19859 60.43928)", +"http://www.yso.fi/onto/yso/p106296":"POINT(6.3925 45.675833333333)", +"http://www.yso.fi/onto/yso/p106297":"POINT(25.66754 61.00231)", +"http://www.yso.fi/onto/yso/p106298":"", +"http://www.yso.fi/onto/yso/p106299":"POINT(25.69068 60.99703)", +"http://www.yso.fi/onto/yso/p106300":"", +"http://www.yso.fi/onto/yso/p106301":"POINT(28.29725 68.29817)", +"http://www.yso.fi/onto/yso/p106303":"POINT(23.945 60.24312)", +"http://www.yso.fi/onto/yso/p106304":"POINT(24.19033 60.37524)", +"http://www.yso.fi/onto/yso/p106305":"POINT(24.26846 60.23213)", +"http://www.yso.fi/onto/yso/p106309":"POINT(22.16145 61.46998)", +"http://www.yso.fi/onto/yso/p106310":"POINT(29.98476 62.85519)", +"http://www.yso.fi/onto/yso/p106312":"", +"http://www.yso.fi/onto/yso/p106316":"POINT(29.55898 65.44792)", +"http://www.yso.fi/onto/yso/p106317":"POINT(21.86316 63.15642)", +"http://www.yso.fi/onto/yso/p106319":"POINT(10.752777777778 59.911111111111)", +"http://www.yso.fi/onto/yso/p106320":"POINT(26.20141 60.62436)", +"http://www.yso.fi/onto/yso/p106321":"POINT(21.98342 62.68636)", +"http://www.yso.fi/onto/yso/p106322":"POINT(24.33266 61.15484)", +"http://www.yso.fi/onto/yso/p106323":"POINT(24.50474 60.97674)", +"http://www.yso.fi/onto/yso/p106328":"POINT(25.20288 66.1713)", +"http://www.yso.fi/onto/yso/p106329":"POINT(27.64444 62.83849)", +"http://www.yso.fi/onto/yso/p106330":"POINT(30.36396 64.1953)", +"http://www.yso.fi/onto/yso/p106340":"POINT(22.17838 62.15368)", +"http://www.yso.fi/onto/yso/p106412":"POINT(26.47799 61.50309)", +"http://www.yso.fi/onto/yso/p106413":"POINT(135.76805555556 35.011666666667)", +"http://www.yso.fi/onto/yso/p106414":"POINT(135.50194444444 34.693611111111)", +"http://www.yso.fi/onto/yso/p106415":"POINT(135.78333333333 34.683333333333)", +"http://www.yso.fi/onto/yso/p106417":"POINT(24.44333 61.02312)", +"http://www.yso.fi/onto/yso/p106419":"POINT(-73.916667 3.816667)", +"http://www.yso.fi/onto/yso/p106420":"POINT(23.64587 59.99943)", +"http://www.yso.fi/onto/yso/p106421":"", +"http://www.yso.fi/onto/yso/p106455":"POINT(21.95331 60.23952)", +"http://www.yso.fi/onto/yso/p106458":"POINT(24.85716 60.2496)", +"http://www.yso.fi/onto/yso/p106459":"POINT(24.9144 60.23425)", +"http://www.yso.fi/onto/yso/p106460":"POINT(24.41443 60.99005)", +"http://www.yso.fi/onto/yso/p106461":"POINT(24.92817 60.23162)", +"http://www.yso.fi/onto/yso/p106463":"POINT(24.98766 60.22632)", +"http://www.yso.fi/onto/yso/p106464":"POINT(24.97006 60.19439)", +"http://www.yso.fi/onto/yso/p106473":"POINT(22.9962 63.8113)", +"http://www.yso.fi/onto/yso/p106476":"POINT(28.56377 66.22191)", +"http://www.yso.fi/onto/yso/p106479":"POINT(22.10304 61.44318)", +"http://www.yso.fi/onto/yso/p106480":"POINT(22.59234 61.59741)", +"http://www.yso.fi/onto/yso/p106503":"POINT(22.34987 63.42006)", +"http://www.yso.fi/onto/yso/p106538":"POINT(27.70944 62.87973)", +"http://www.yso.fi/onto/yso/p106558":"POINT(22.24114 60.44575)", +"http://www.yso.fi/onto/yso/p106559":"POINT(25.53746 65.00455)", +"http://www.yso.fi/onto/yso/p106560":"POINT(24.88958 60.20453)", +"http://www.yso.fi/onto/yso/p106561":"POINT(28 36.166666666667)", +"http://www.yso.fi/onto/yso/p106562":"POINT(24.83993 63.66307)", +"http://www.yso.fi/onto/yso/p106564":"POINT(25.88853 62.24674)", +"http://www.yso.fi/onto/yso/p106569":"POINT(28.858333333333 61.108333333333)", +"http://www.yso.fi/onto/yso/p106570":"", +"http://www.yso.fi/onto/yso/p106571":"POINT(28.48825 62.81241)", +"http://www.yso.fi/onto/yso/p106572":"", +"http://www.yso.fi/onto/yso/p106575":"POINT(28.64284 62.54745)", +"http://www.yso.fi/onto/yso/p106576":"POINT(25.01136 60.25172)", +"http://www.yso.fi/onto/yso/p106577":"POINT(22.55612 61.66359)", +"http://www.yso.fi/onto/yso/p106578":"POINT(28.666666666667 60.533333333333)", +"http://www.yso.fi/onto/yso/p106589":"POINT(25.06216 61.24241)", +"http://www.yso.fi/onto/yso/p106590":"POINT(30.70018 63.16647)", +"http://www.yso.fi/onto/yso/p106591":"POINT(30.69244 63.16414)", +"http://www.yso.fi/onto/yso/p106662":"POINT(27 60.066666666667)", +"http://www.yso.fi/onto/yso/p106663":"POINT(29.50107 61.55513)", +"http://www.yso.fi/onto/yso/p106667":"POINT(24.92509 63.57746)", +"http://www.yso.fi/onto/yso/p106669":"POINT(23.66049 66.42933)", +"http://www.yso.fi/onto/yso/p106670":"POINT(21.51793 62.07815)", +"http://www.yso.fi/onto/yso/p106673":"POINT(25.5 42.75)", +"http://www.yso.fi/onto/yso/p106675":"POINT(28.02895 62.76167)", +"http://www.yso.fi/onto/yso/p106676":"POINT(25.29591 64.87588)", +"http://www.yso.fi/onto/yso/p106677":"POINT(22.436944 63.453333)", +"http://www.yso.fi/onto/yso/p106678":"POINT(27.87136 70.08361)", +"http://www.yso.fi/onto/yso/p106679":"POINT(29.65883 62.63193)", +"http://www.yso.fi/onto/yso/p106680":"POINT(29.5621 62.61777)", +"http://www.yso.fi/onto/yso/p106681":"POINT(22.7116 59.94352)", +"http://www.yso.fi/onto/yso/p106682":"POINT(24.9595 62.93643)", +"http://www.yso.fi/onto/yso/p106683":"POINT(24.32591 60.33547)", +"http://www.yso.fi/onto/yso/p106684":"POINT(25.983333333333 63.683333333333)", +"http://www.yso.fi/onto/yso/p106685":"POINT(-94 46)", +"http://www.yso.fi/onto/yso/p106686":"POINT(21.83931 63.16181)", +"http://www.yso.fi/onto/yso/p106700":"", +"http://www.yso.fi/onto/yso/p106711":"POINT(29.15161 66.16563)", +"http://www.yso.fi/onto/yso/p106714":"POINT(28.4342 60.7881)", +"http://www.yso.fi/onto/yso/p106716":"POINT(26.72584 63.47575)", +"http://www.yso.fi/onto/yso/p106717":"POINT(26.72145 63.44517)", +"http://www.yso.fi/onto/yso/p106720":"POINT(24.76777 60.81739)", +"http://www.yso.fi/onto/yso/p106721":"POINT(105 60)", +"http://www.yso.fi/onto/yso/p106722":"POINT(23.14123 61.75955)", +"http://www.yso.fi/onto/yso/p106723":"POINT(29.20023 64.13032)", +"http://www.yso.fi/onto/yso/p106812":"POINT(25.98578 63.68184)", +"http://www.yso.fi/onto/yso/p106813":"POINT(17.65 59.85)", +"http://www.yso.fi/onto/yso/p106814":"POINT(25.77115 62.22737)", +"http://www.yso.fi/onto/yso/p106815":"POINT(4.9 52.383333333333)", +"http://www.yso.fi/onto/yso/p106816":"POINT(23.716666666667 38)", +"http://www.yso.fi/onto/yso/p106817":"POINT(28.2406 65.57625)", +"http://www.yso.fi/onto/yso/p106818":"POINT(31.6 61.35)", +"http://www.yso.fi/onto/yso/p106819":"POINT(26.84067 64.56094)", +"http://www.yso.fi/onto/yso/p106820":"POINT(11.254166666667 43.771388888889)", +"http://www.yso.fi/onto/yso/p106879":"POINT(35.233333333333 31.783333333333)", +"http://www.yso.fi/onto/yso/p106880":"POINT(1.45 38.7)", +"http://www.yso.fi/onto/yso/p106881":"POINT(27.38742 63.36452)", +"http://www.yso.fi/onto/yso/p106882":"POINT(27.859166666667 60.465555555556)", +"http://www.yso.fi/onto/yso/p106883":"POINT(27.47356 63.61667)", +"http://www.yso.fi/onto/yso/p106887":"POINT(26.92486 60.88785)", +"http://www.yso.fi/onto/yso/p106924":"POINT(-61.35 14.233055555556)", +"http://www.yso.fi/onto/yso/p106937":"POINT(24.94822 60.21288)", +"http://www.yso.fi/onto/yso/p106944":"POINT(27.61018 68.70491)", +"http://www.yso.fi/onto/yso/p106952":"POINT(-17 32.75)", +"http://www.yso.fi/onto/yso/p106953":"POINT(24.96985 60.20462)", +"http://www.yso.fi/onto/yso/p106954":"POINT(12.568888888889 55.676111111111)", +"http://www.yso.fi/onto/yso/p106955":"POINT(25.14534 60.39903)", +"http://www.yso.fi/onto/yso/p106956":"POINT(16.373064 48.20833)", +"http://www.yso.fi/onto/yso/p106957":"POINT(24.85963 60.21197)", +"http://www.yso.fi/onto/yso/p106958":"POINT(29.62606 62.31268)", +"http://www.yso.fi/onto/yso/p106959":"POINT(23.38984 60.65167)", +"http://www.yso.fi/onto/yso/p106971":"POINT(22.16117 60.42908)", +"http://www.yso.fi/onto/yso/p106972":"POINT(31.12059 62.44712)", +"http://www.yso.fi/onto/yso/p106973":"POINT(24.58246 65.67501)", +"http://www.yso.fi/onto/yso/p106976":"POINT(26.79896 68.99193)", +"http://www.yso.fi/onto/yso/p106977":"POINT(23.86589 61.16715)", +"http://www.yso.fi/onto/yso/p106983":"POINT(23.6975 61.16148)", +"http://www.yso.fi/onto/yso/p106997":"", +"http://www.yso.fi/onto/yso/p106998":"POINT(26.80115 61.20732)", +"http://www.yso.fi/onto/yso/p106999":"POINT(33 63.816666666667)", +"http://www.yso.fi/onto/yso/p107015":"POINT(24.79434 61.83305)", +"http://www.yso.fi/onto/yso/p107016":"POINT(26.07746 60.31219)", +"http://www.yso.fi/onto/yso/p107018":"POINT(28.41334 62.74149)", +"http://www.yso.fi/onto/yso/p107019":"POINT(30.15179 63.32026)", +"http://www.yso.fi/onto/yso/p107020":"POINT(28.566666666667 60.616666666667)", +"http://www.yso.fi/onto/yso/p107068":"POINT(-86.816667 14.633333)", +"http://www.yso.fi/onto/yso/p107069":"POINT(-84 10)", +"http://www.yso.fi/onto/yso/p107112":"POINT(-77.3217773 18.1823878)", +"http://www.yso.fi/onto/yso/p107113":"POINT(24.18 65.86113)", +"http://www.yso.fi/onto/yso/p107114":"POINT(24.17513 65.83694)", +"http://www.yso.fi/onto/yso/p107116":"POINT(25.18361 62.25666)", +"http://www.yso.fi/onto/yso/p107117":"POINT(26.06902 60.74549)", +"http://www.yso.fi/onto/yso/p107118":"POINT(25.73755 62.24166)", +"http://www.yso.fi/onto/yso/p107128":"POINT(-0.11666666666667 51.5)", +"http://www.yso.fi/onto/yso/p107232":"POINT(22.00542 63.18204)", +"http://www.yso.fi/onto/yso/p107251":"POINT(25.6645 64.50613)", +"http://www.yso.fi/onto/yso/p107252":"POINT(23.71932 60.24181)", +"http://www.yso.fi/onto/yso/p107253":"POINT(-4.48 36.79)", +"http://www.yso.fi/onto/yso/p107254":"POINT(-6.2658333333333 53.3425)", +"http://www.yso.fi/onto/yso/p107255":"POINT(-30 0)", +"http://www.yso.fi/onto/yso/p107306":"", +"http://www.yso.fi/onto/yso/p107330":"POINT(22.90989 62.8287)", +"http://www.yso.fi/onto/yso/p107369":"POINT(22.12306667 60.94355)", +"http://www.yso.fi/onto/yso/p107390":"POINT(-70.01166667 -32.65333333)", +"http://www.yso.fi/onto/yso/p107391":"POINT(-70.011666666667 -32.653333333333)", +"http://www.yso.fi/onto/yso/p107479":"POINT(20 49)", +"http://www.yso.fi/onto/yso/p107480":"POINT(16 50)", +"http://www.yso.fi/onto/yso/p107482":"POINT(36.616667 31.016667)", +"http://www.yso.fi/onto/yso/p107484":"", +"http://www.yso.fi/onto/yso/p107485":"POINT(24.90366 60.17344)", +"http://www.yso.fi/onto/yso/p107486":"POINT(26.7225 58.38)", +"http://www.yso.fi/onto/yso/p107487":"POINT(25.04319 60.16222)", +"http://www.yso.fi/onto/yso/p107488":"", +"http://www.yso.fi/onto/yso/p107492":"POINT(29.805555555556 61.058333333333)", +"http://www.yso.fi/onto/yso/p107493":"POINT(30.1275 61.518333333333)", +"http://www.yso.fi/onto/yso/p107494":"POINT(22.38509 60.45481)", +"http://www.yso.fi/onto/yso/p107496":"POINT(31.38229 62.80965)", +"http://www.yso.fi/onto/yso/p107500":"POINT(25.62296 60.96544)", +"http://www.yso.fi/onto/yso/p107501":"POINT(22.47878 61.84946)", +"http://www.yso.fi/onto/yso/p107502":"POINT(23.27963 60.34947)", +"http://www.yso.fi/onto/yso/p107503":"POINT(21.52924 62.86911)", +"http://www.yso.fi/onto/yso/p107504":"POINT(22.9003 60.21696)", +"http://www.yso.fi/onto/yso/p107591":"POINT(30.006944444444 61.786111111111)", +"http://www.yso.fi/onto/yso/p107592":"POINT(20.933333333333 43.95)", +"http://www.yso.fi/onto/yso/p107593":"POINT(19.216667 42.766667)", +"http://www.yso.fi/onto/yso/p107594":"POINT(15 46)", +"http://www.yso.fi/onto/yso/p107595":"POINT(15.466667 45.25)", +"http://www.yso.fi/onto/yso/p107596":"POINT(21.716667 41.65)", +"http://www.yso.fi/onto/yso/p107622":"POINT(21.61717 63.0848)", +"http://www.yso.fi/onto/yso/p107623":"POINT(23.77957 61.50096)", +"http://www.yso.fi/onto/yso/p107624":"POINT(24.33956 60.44755)", +"http://www.yso.fi/onto/yso/p107633":"POINT(26.42635 68.79745)", +"http://www.yso.fi/onto/yso/p107635":"POINT(21.67036 62.68496)", +"http://www.yso.fi/onto/yso/p107637":"POINT(31.148987 61.914069)", +"http://www.yso.fi/onto/yso/p107638":"POINT(26.94304 62.19066)", +"http://www.yso.fi/onto/yso/p107639":"POINT(24.76082 60.23254)", +"http://www.yso.fi/onto/yso/p107640":"POINT(21.33011 63.34665)", +"http://www.yso.fi/onto/yso/p107643":"POINT(17.35 -12.35)", +"http://www.yso.fi/onto/yso/p107644":"POINT(31.433333 -26.483333)", +"http://www.yso.fi/onto/yso/p107645":"POINT(28.366666666667 60.033333333333)", +"http://www.yso.fi/onto/yso/p107648":"POINT(25.60058 60.26431)", +"http://www.yso.fi/onto/yso/p107650":"POINT(63.9 45.3)", +"http://www.yso.fi/onto/yso/p107651":"POINT(89 49)", +"http://www.yso.fi/onto/yso/p107655":"POINT(-92 31)", +"http://www.yso.fi/onto/yso/p107662":"POINT(44.95 40.383333)", +"http://www.yso.fi/onto/yso/p107663":"POINT(47.7 40.3)", +"http://www.yso.fi/onto/yso/p107664":"POINT(32 49)", +"http://www.yso.fi/onto/yso/p107665":"POINT(68 48)", +"http://www.yso.fi/onto/yso/p107666":"POINT(74.983333 41.266667)", +"http://www.yso.fi/onto/yso/p107667":"POINT(28.516667 47.25)", +"http://www.yso.fi/onto/yso/p107668":"POINT(71.366667 38.583333)", +"http://www.yso.fi/onto/yso/p107669":"POINT(59.65 39.333333)", +"http://www.yso.fi/onto/yso/p107670":"POINT(25.5 47)", +"http://www.yso.fi/onto/yso/p107671":"POINT(40 75)", +"http://www.yso.fi/onto/yso/p107675":"POINT(23.70951 61.50255)", +"http://www.yso.fi/onto/yso/p107680":"POINT(21.80273 62.19005)", +"http://www.yso.fi/onto/yso/p107681":"POINT(-65 8)", +"http://www.yso.fi/onto/yso/p107682":"POINT(86.733333333333 26.533333333333)", +"http://www.yso.fi/onto/yso/p107683":"POINT(29.689166666667 61.24)", +"http://www.yso.fi/onto/yso/p107684":"POINT(25.05108 60.32974)", +"http://www.yso.fi/onto/yso/p107685":"POINT(25.07444 60.32674)", +"http://www.yso.fi/onto/yso/p107691":"POINT(24.70159 60.29668)", +"http://www.yso.fi/onto/yso/p107712":"POINT(24.85677 67.78387)", +"http://www.yso.fi/onto/yso/p107717":"POINT(20.5 54.716666666667)", +"http://www.yso.fi/onto/yso/p107719":"POINT(-118.25 34.05)", +"http://www.yso.fi/onto/yso/p107720":"POINT(-111.88333333333 40.75)", +"http://www.yso.fi/onto/yso/p107721":"POINT(-115.13638888889 36.175)", +"http://www.yso.fi/onto/yso/p107722":"POINT(-117.1625 32.715)", +"http://www.yso.fi/onto/yso/p107723":"POINT(-100 31)", +"http://www.yso.fi/onto/yso/p107725":"POINT(22.90644 61.34023)", +"http://www.yso.fi/onto/yso/p107726":"POINT(30.2375 60.551388888889)", +"http://www.yso.fi/onto/yso/p107727":"", +"http://www.yso.fi/onto/yso/p107731":"POINT(24.05343 61.20408)", +"http://www.yso.fi/onto/yso/p107734":"POINT(25.55905 62.01607)", +"http://www.yso.fi/onto/yso/p107735":"POINT(25.46838 62.14672)", +"http://www.yso.fi/onto/yso/p107736":"POINT(11.916667 5.116667)", +"http://www.yso.fi/onto/yso/p107738":"POINT(27.30016 63.15574)", +"http://www.yso.fi/onto/yso/p107739":"POINT(27.27458 63.12193)", +"http://www.yso.fi/onto/yso/p107748":"POINT(30.608333333333 60.858333333333)", +"http://www.yso.fi/onto/yso/p107754":"POINT(25.47905 61.03806)", +"http://www.yso.fi/onto/yso/p107756":"POINT(-109.35472222222 -27.119444444444)", +"http://www.yso.fi/onto/yso/p107757":"POINT(29.01569 69.70721)", +"http://www.yso.fi/onto/yso/p107758":"POINT(24.326388888889 40.157222222222)", +"http://www.yso.fi/onto/yso/p107759":"POINT(23.43 61.04229)", +"http://www.yso.fi/onto/yso/p107766":"POINT(23.88962 66.64063)", +"http://www.yso.fi/onto/yso/p107770":"POINT(26.77174 64.52637)", +"http://www.yso.fi/onto/yso/p107771":"POINT(27.18299 64.59074)", +"http://www.yso.fi/onto/yso/p107802":"", +"http://www.yso.fi/onto/yso/p107803":"POINT(24.7447 60.38584)", +"http://www.yso.fi/onto/yso/p107804":"POINT(24.745 59.437222222222)", +"http://www.yso.fi/onto/yso/p107805":"POINT(27.55734 62.94562)", +"http://www.yso.fi/onto/yso/p107811":"POINT(25.44582 66.62725)", +"http://www.yso.fi/onto/yso/p107823":"POINT(23.58392 61.54832)", +"http://www.yso.fi/onto/yso/p107824":"POINT(28.15139 65.85439)", +"http://www.yso.fi/onto/yso/p107827":"POINT(24.95122 60.20872)", +"http://www.yso.fi/onto/yso/p107848":"POINT(21.40615 61.6339)", +"http://www.yso.fi/onto/yso/p107886":"POINT(24.498888888889 58.384444444444)", +"http://www.yso.fi/onto/yso/p107887":"POINT(24.75743 60.2402)", +"http://www.yso.fi/onto/yso/p107888":"POINT(25.69449 66.5304)", +"http://www.yso.fi/onto/yso/p107890":"POINT(24.58843 60.85618)", +"http://www.yso.fi/onto/yso/p107891":"POINT(22.91295 59.81053)", +"http://www.yso.fi/onto/yso/p107892":"POINT(26.992777777778 57.848611111111)", +"http://www.yso.fi/onto/yso/p107898":"POINT(22.17 61.26793)", +"http://www.yso.fi/onto/yso/p107902":"POINT(-13.633 29.035)", +"http://www.yso.fi/onto/yso/p107905":"POINT(27.37588 66.72543)", +"http://www.yso.fi/onto/yso/p107906":"POINT(24.15972 64.20442)", +"http://www.yso.fi/onto/yso/p107912":"", +"http://www.yso.fi/onto/yso/p107914":"POINT(-160 0)", +"http://www.yso.fi/onto/yso/p107915":"POINT(27.32893 65.79901)", +"http://www.yso.fi/onto/yso/p107916":"POINT(23.67574 66.29288)", +"http://www.yso.fi/onto/yso/p107917":"POINT(22.86616 63.57121)", +"http://www.yso.fi/onto/yso/p107918":"POINT(28.52602 61.02749)", +"http://www.yso.fi/onto/yso/p107919":"POINT(25.05006 65.63929)", +"http://www.yso.fi/onto/yso/p107922":"POINT(25.12687 60.37203)", +"http://www.yso.fi/onto/yso/p107923":"POINT(27.80666 62.91108)", +"http://www.yso.fi/onto/yso/p107924":"POINT(30.49666 62.44374)", +"http://www.yso.fi/onto/yso/p107927":"POINT(24.05756 61.40501)", +"http://www.yso.fi/onto/yso/p107933":"POINT(26.05107 60.5703)", +"http://www.yso.fi/onto/yso/p107944":"POINT(29.50883 66.29736)", +"http://www.yso.fi/onto/yso/p107950":"", +"http://www.yso.fi/onto/yso/p107955":"POINT(23.74463 61.50041)", +"http://www.yso.fi/onto/yso/p107956":"POINT(23.66952 61.50731)", +"http://www.yso.fi/onto/yso/p107957":"POINT(29.27352 63.51779)", +"http://www.yso.fi/onto/yso/p107958":"POINT(25.52916 61.27949)", +"http://www.yso.fi/onto/yso/p107959":"POINT(28.14562 63.6941)", +"http://www.yso.fi/onto/yso/p107960":"POINT(25.19612 67.61578)", +"http://www.yso.fi/onto/yso/p107961":"POINT(21.74773 63.43633)", +"http://www.yso.fi/onto/yso/p107962":"POINT(27.66019 60.60418)", +"http://www.yso.fi/onto/yso/p107963":"POINT(26.31463 68.81594)", +"http://www.yso.fi/onto/yso/p107965":"POINT(24.0497 68.07571)", +"http://www.yso.fi/onto/yso/p107966":"POINT(23.80834 67.92415)", +"http://www.yso.fi/onto/yso/p107968":"", +"http://www.yso.fi/onto/yso/p107969":"", +"http://www.yso.fi/onto/yso/p107970":"POINT(29.66073 61.69501)", +"http://www.yso.fi/onto/yso/p107971":"", +"http://www.yso.fi/onto/yso/p107972":"POINT(30.61544 62.201)", +"http://www.yso.fi/onto/yso/p107973":"", +"http://www.yso.fi/onto/yso/p107977":"POINT(20.61444444 63.23583333)", +"http://www.yso.fi/onto/yso/p107978":"POINT(43.5 33.7)", +"http://www.yso.fi/onto/yso/p107979":"POINT(43.3 36)", +"http://www.yso.fi/onto/yso/p107980":"", +"http://www.yso.fi/onto/yso/p107982":"POINT(22.33063 60.45978)", +"http://www.yso.fi/onto/yso/p107983":"POINT(22.32777778 60.46111111)", +"http://www.yso.fi/onto/yso/p107984":"POINT(24.96231 60.36704)", +"http://www.yso.fi/onto/yso/p107990":"POINT(18 44)", +"http://www.yso.fi/onto/yso/p107991":"POINT(24.32701 61.74291)", +"http://www.yso.fi/onto/yso/p107999":"POINT(116.39138888889 39.905)", +"http://www.yso.fi/onto/yso/p108002":"POINT(34.35 61.783333333333)", +"http://www.yso.fi/onto/yso/p108003":"POINT(32.233753 61.677088)", +"http://www.yso.fi/onto/yso/p108005":"POINT(28.960277777778 41.01)", +"http://www.yso.fi/onto/yso/p108006":"POINT(28.976018 41.01224)", +"http://www.yso.fi/onto/yso/p108007":"POINT(-64 -34)", +"http://www.yso.fi/onto/yso/p108008":"POINT(24.89029 60.19051)", +"http://www.yso.fi/onto/yso/p108010":"POINT(24.94866 60.24327)", +"http://www.yso.fi/onto/yso/p108011":"POINT(9.1775 48.776111111111)", +"http://www.yso.fi/onto/yso/p108013":"POINT(26.39144 62.7724)", +"http://www.yso.fi/onto/yso/p108015":"POINT(23.24748 63.05075)", +"http://www.yso.fi/onto/yso/p108031":"POINT(28.13883 68.21847)", +"http://www.yso.fi/onto/yso/p108032":"POINT(28.28077 68.41626)", +"http://www.yso.fi/onto/yso/p108033":"POINT(24.36887 66.58587)", +"http://www.yso.fi/onto/yso/p108038":"POINT(24.87109 60.34349)", +"http://www.yso.fi/onto/yso/p108039":"POINT(24.40076 60.3287)", +"http://www.yso.fi/onto/yso/p108041":"POINT(29.47201 62.58112)", +"http://www.yso.fi/onto/yso/p108042":"", +"http://www.yso.fi/onto/yso/p108043":"POINT(32 15)", +"http://www.yso.fi/onto/yso/p108044":"POINT(25.4578 60.88661)", +"http://www.yso.fi/onto/yso/p108045":"POINT(24.16802 60.41687)", +"http://www.yso.fi/onto/yso/p108046":"POINT(29.04467 62.69105)", +"http://www.yso.fi/onto/yso/p108047":"POINT(30.62463 62.21342)", +"http://www.yso.fi/onto/yso/p108049":"POINT(29.75581 62.30542)", +"http://www.yso.fi/onto/yso/p108050":"POINT(29.5832 63.16887)", +"http://www.yso.fi/onto/yso/p108051":"POINT(22.2604 62.43499)", +"http://www.yso.fi/onto/yso/p108052":"POINT(27.36819 62.65049)", +"http://www.yso.fi/onto/yso/p108054":"POINT(29.60011 64.23887)", +"http://www.yso.fi/onto/yso/p108056":"POINT(22.15359 61.11296)", +"http://www.yso.fi/onto/yso/p108057":"POINT(21.78617 61.40334)", +"http://www.yso.fi/onto/yso/p108058":"POINT(20.29325 60.20844)", +"http://www.yso.fi/onto/yso/p108075":"POINT(14.666389 68.333056)", +"http://www.yso.fi/onto/yso/p108076":"POINT(18.55 57.5)", +"http://www.yso.fi/onto/yso/p108077":"POINT(19.82 39.623888888889)", +"http://www.yso.fi/onto/yso/p108083":"POINT(21.44041 60.86942)", +"http://www.yso.fi/onto/yso/p108084":"POINT(21.63518 60.82816)", +"http://www.yso.fi/onto/yso/p108085":"", +"http://www.yso.fi/onto/yso/p108087":"POINT(9.0833333333333 42.15)", +"http://www.yso.fi/onto/yso/p108088":"POINT(11.016666666667 43.35)", +"http://www.yso.fi/onto/yso/p108089":"POINT(1.47 41.81)", +"http://www.yso.fi/onto/yso/p108090":"POINT(-55.777222222222 48.566388888889)", +"http://www.yso.fi/onto/yso/p108091":"POINT(48 15)", +"http://www.yso.fi/onto/yso/p108094":"", +"http://www.yso.fi/onto/yso/p108095":"POINT(27.45301 63.65018)", +"http://www.yso.fi/onto/yso/p108096":"POINT(22.8659 63.07686)", +"http://www.yso.fi/onto/yso/p108098":"POINT(22.5 58.416666666667)", +"http://www.yso.fi/onto/yso/p108099":"POINT(23.583333333333 46.766666666667)", +"http://www.yso.fi/onto/yso/p108100":"", +"http://www.yso.fi/onto/yso/p108101":"POINT(24.76771 60.19191)", +"http://www.yso.fi/onto/yso/p108102":"POINT(23.82505 60.32014)", +"http://www.yso.fi/onto/yso/p108103":"POINT(24.58724 60.28224)", +"http://www.yso.fi/onto/yso/p108105":"POINT(25.53979 60.49533)", +"http://www.yso.fi/onto/yso/p108109":"POINT(37.661666666667 -6.8230555555556)", +"http://www.yso.fi/onto/yso/p108110":"POINT(140 -5)", +"http://www.yso.fi/onto/yso/p108111":"POINT(-71 -31)", +"http://www.yso.fi/onto/yso/p108118":"POINT(23.63344 62.20983)", +"http://www.yso.fi/onto/yso/p108120":"", +"http://www.yso.fi/onto/yso/p108121":"", +"http://www.yso.fi/onto/yso/p108122":"", +"http://www.yso.fi/onto/yso/p108125":"POINT(25.01095 60.34051)", +"http://www.yso.fi/onto/yso/p108126":"POINT(-53 -11)", +"http://www.yso.fi/onto/yso/p108128":"POINT(-5.9833333333333 37.383333333333)", +"http://www.yso.fi/onto/yso/p108129":"POINT(-2.9736 37.8392)", +"http://www.yso.fi/onto/yso/p108130":"POINT(-4.4166666666667 36.716666666667)", +"http://www.yso.fi/onto/yso/p108131":"POINT(8.8088888888889 53.076944444444)", +"http://www.yso.fi/onto/yso/p108151":"POINT(24.15134 61.30229)", +"http://www.yso.fi/onto/yso/p108152":"POINT(24.34204 61.37164)", +"http://www.yso.fi/onto/yso/p108153":"POINT(31.49 62.288055555556)", +"http://www.yso.fi/onto/yso/p108155":"POINT(21.43048 61.61219)", +"http://www.yso.fi/onto/yso/p108156":"POINT(24.59265 62.58878)", +"http://www.yso.fi/onto/yso/p108157":"POINT(21.77971 63.2232)", +"http://www.yso.fi/onto/yso/p108158":"POINT(22.33487 60.44573)", +"http://www.yso.fi/onto/yso/p108159":"POINT(25.08426 60.2072)", +"http://www.yso.fi/onto/yso/p108160":"POINT(24.94292 60.18989)", +"http://www.yso.fi/onto/yso/p108161":"POINT(10 51)", +"http://www.yso.fi/onto/yso/p108162":"POINT(28.67912 62.92708)", +"http://www.yso.fi/onto/yso/p108163":"POINT(25.96555 64.17763)", +"http://www.yso.fi/onto/yso/p108164":"POINT(28.65185 62.51275)", +"http://www.yso.fi/onto/yso/p108165":"POINT(28.7207 62.50373)", +"http://www.yso.fi/onto/yso/p108166":"POINT(23.20516 63.76071)", +"http://www.yso.fi/onto/yso/p108167":"POINT(24.19361 65.81513)", +"http://www.yso.fi/onto/yso/p108168":"POINT(24.19361 65.81513)", +"http://www.yso.fi/onto/yso/p108179":"POINT(24.6456 60.81481)", +"http://www.yso.fi/onto/yso/p108180":"POINT(-10.766667 10.966667)", +"http://www.yso.fi/onto/yso/p108183":"POINT(25.0407 62.80891)", +"http://www.yso.fi/onto/yso/p108184":"POINT(30.116666666667 61.033333333333)", +"http://www.yso.fi/onto/yso/p108185":"POINT(29.989166666667 61.033333333333)", +"http://www.yso.fi/onto/yso/p108187":"POINT(27.7892 62.60345)", +"http://www.yso.fi/onto/yso/p108188":"POINT(27.82067 62.5782)", +"http://www.yso.fi/onto/yso/p108190":"POINT(26.83616 62.52506)", +"http://www.yso.fi/onto/yso/p108191":"POINT(25.02156 60.26456)", +"http://www.yso.fi/onto/yso/p108192":"POINT(24.95011 60.17721)", +"http://www.yso.fi/onto/yso/p108193":"POINT(22.39136 60.44877)", +"http://www.yso.fi/onto/yso/p108194":"POINT(22.39136 60.44877)", +"http://www.yso.fi/onto/yso/p108195":"POINT(22.39136 60.44877)", +"http://www.yso.fi/onto/yso/p108198":"POINT(28.27143 64.12576)", +"http://www.yso.fi/onto/yso/p108199":"POINT(29.4038 61.51504)", +"http://www.yso.fi/onto/yso/p108223":"POINT(22.18725 61.51405)", +"http://www.yso.fi/onto/yso/p108224":"POINT(22.14017 61.51601)", +"http://www.yso.fi/onto/yso/p108225":"POINT(25.29046 64.98609)", +"http://www.yso.fi/onto/yso/p108226":"POINT(26.15749 65.14093)", +"http://www.yso.fi/onto/yso/p108227":"POINT(9.0411111111111 48.537777777778)", +"http://www.yso.fi/onto/yso/p108228":"POINT(9.9833333333333 48.4)", +"http://www.yso.fi/onto/yso/p108232":"POINT(27.55422 60.52667)", +"http://www.yso.fi/onto/yso/p108233":"POINT(25.07127 60.35538)", +"http://www.yso.fi/onto/yso/p108234":"POINT(29.488888888889 60.543055555556)", +"http://www.yso.fi/onto/yso/p108235":"POINT(23.82939 61.46309)", +"http://www.yso.fi/onto/yso/p108244":"POINT(21.88802 63.10362)", +"http://www.yso.fi/onto/yso/p108246":"POINT(8.6797222222222 50.113611111111)", +"http://www.yso.fi/onto/yso/p108250":"POINT(24.26494 61.95254)", +"http://www.yso.fi/onto/yso/p108251":"POINT(27.49307 61.12454)", +"http://www.yso.fi/onto/yso/p108252":"POINT(23.36707 60.92201)", +"http://www.yso.fi/onto/yso/p108253":"POINT(27.90743 61.71991)", +"http://www.yso.fi/onto/yso/p108254":"POINT(25.5326 63.04702)", +"http://www.yso.fi/onto/yso/p108255":"POINT(24.5565 64.80474)", +"http://www.yso.fi/onto/yso/p108256":"POINT(27.6518 64.28101)", +"http://www.yso.fi/onto/yso/p108260":"POINT(24.65939 60.69054)", +"http://www.yso.fi/onto/yso/p108262":"POINT(30.6 64.583333333333)", +"http://www.yso.fi/onto/yso/p108274":"POINT(29.416666666667 62.15)", +"http://www.yso.fi/onto/yso/p108276":"POINT(26.28236 66.73005)", +"http://www.yso.fi/onto/yso/p108277":"POINT(27.97385 63.03273)", +"http://www.yso.fi/onto/yso/p108278":"POINT(26.59192 63.11724)", +"http://www.yso.fi/onto/yso/p108279":"POINT(22.00408 61.3669)", +"http://www.yso.fi/onto/yso/p108283":"POINT(25.75752 60.56785)", +"http://www.yso.fi/onto/yso/p108286":"POINT(24.44199 60.04779)", +"http://www.yso.fi/onto/yso/p108287":"POINT(25.044 60.2893)", +"http://www.yso.fi/onto/yso/p108289":"POINT(21.44216 62.61676)", +"http://www.yso.fi/onto/yso/p108291":"POINT(29.59971 66.29722)", +"http://www.yso.fi/onto/yso/p108292":"POINT(113.55 22.166666666667)", +"http://www.yso.fi/onto/yso/p108296":"POINT(40.533333333333 64.55)", +"http://www.yso.fi/onto/yso/p108297":"POINT(54.833333333333 68.833333333333)", +"http://www.yso.fi/onto/yso/p108298":"POINT(57.5 -20.2)", +"http://www.yso.fi/onto/yso/p108299":"POINT(24.92447 60.16006)", +"http://www.yso.fi/onto/yso/p108302":"POINT(28.04537 65.14242)", +"http://www.yso.fi/onto/yso/p108305":"POINT(23.75253 61.47305)", +"http://www.yso.fi/onto/yso/p108306":"POINT(23.73099 61.46686)", +"http://www.yso.fi/onto/yso/p108307":"POINT(26.94531 61.05801)", +"http://www.yso.fi/onto/yso/p108311":"POINT(28.90533 63.53172)", +"http://www.yso.fi/onto/yso/p108317":"POINT(22.72587 60.72433)", +"http://www.yso.fi/onto/yso/p108324":"POINT(-2.0666666666667 12.266666666667)", +"http://www.yso.fi/onto/yso/p108325":"POINT(-2.066667 12.266667)", +"http://www.yso.fi/onto/yso/p108330":"POINT(20.79845 69.04836)", +"http://www.yso.fi/onto/yso/p108331":"POINT(28.12134 61.0493)", +"http://www.yso.fi/onto/yso/p108332":"POINT(28.55104 60.96011)", +"http://www.yso.fi/onto/yso/p108333":"POINT(15.383330555556 -0.75)", +"http://www.yso.fi/onto/yso/p108334":"POINT(25.65935 60.37564)", +"http://www.yso.fi/onto/yso/p108335":"POINT(26.54973 63.40018)", +"http://www.yso.fi/onto/yso/p108337":"POINT(24.51976 60.38833)", +"http://www.yso.fi/onto/yso/p108338":"POINT(28.55104 60.96011)", +"http://www.yso.fi/onto/yso/p108351":"POINT(21.04371 60.41266)", +"http://www.yso.fi/onto/yso/p108352":"POINT(22.01045 61.69485)", +"http://www.yso.fi/onto/yso/p108353":"POINT(20.00897 60.11739)", +"http://www.yso.fi/onto/yso/p108354":"POINT(21.44093 60.94827)", +"http://www.yso.fi/onto/yso/p108359":"POINT(24.15402 61.39614)", +"http://www.yso.fi/onto/yso/p108360":"POINT(24.23115 60.27747)", +"http://www.yso.fi/onto/yso/p108361":"POINT(24.14408 60.29294)", +"http://www.yso.fi/onto/yso/p108363":"POINT(23.07949 63.72113)", +"http://www.yso.fi/onto/yso/p108368":"", +"http://www.yso.fi/onto/yso/p108374":"", +"http://www.yso.fi/onto/yso/p108395":"POINT(27.06378 61.09163)", +"http://www.yso.fi/onto/yso/p108396":"POINT(26.83778 61.04529)", +"http://www.yso.fi/onto/yso/p108397":"POINT(30.116 62.64866)", +"http://www.yso.fi/onto/yso/p108398":"POINT(23.68637 61.50095)", +"http://www.yso.fi/onto/yso/p108400":"POINT(28.16437 67.292)", +"http://www.yso.fi/onto/yso/p108401":"POINT(27.54109 67.5277)", +"http://www.yso.fi/onto/yso/p108402":"POINT(24.98157 60.22965)", +"http://www.yso.fi/onto/yso/p108403":"POINT(18.416666666667 43.866666666667)", +"http://www.yso.fi/onto/yso/p108404":"POINT(14.9 58.45)", +"http://www.yso.fi/onto/yso/p108405":"POINT(23.37061 62.27745)", +"http://www.yso.fi/onto/yso/p108406":"", +"http://www.yso.fi/onto/yso/p108417":"POINT(18 35)", +"http://www.yso.fi/onto/yso/p108418":"POINT(35 44)", +"http://www.yso.fi/onto/yso/p108420":"POINT(23.756 61.19058)", +"http://www.yso.fi/onto/yso/p108424":"POINT(21.66283 62.77782)", +"http://www.yso.fi/onto/yso/p108431":"POINT(25.20643 64.60715)", +"http://www.yso.fi/onto/yso/p108432":"POINT(25.09805 64.66477)", +"http://www.yso.fi/onto/yso/p108433":"POINT(24.9628 64.72164)", +"http://www.yso.fi/onto/yso/p108435":"POINT(25.10238 60.23229)", +"http://www.yso.fi/onto/yso/p108436":"POINT(3.7166666666667 51.05)", +"http://www.yso.fi/onto/yso/p108437":"POINT(-2.2333333333333 53.466666666667)", +"http://www.yso.fi/onto/yso/p108438":"POINT(-0.57944444444444 44.837777777778)", +"http://www.yso.fi/onto/yso/p108440":"POINT(29.77994 67.57262)", +"http://www.yso.fi/onto/yso/p108443":"POINT(25.55856 60.98039)", +"http://www.yso.fi/onto/yso/p108444":"POINT(24.99088 60.15419)", +"http://www.yso.fi/onto/yso/p108445":"POINT(25.65138 60.99956)", +"http://www.yso.fi/onto/yso/p108450":"POINT(10.4325 61.13)", +"http://www.yso.fi/onto/yso/p108460":"POINT(23.01308 61.95451)", +"http://www.yso.fi/onto/yso/p108462":"POINT(30.41584 63.4284)", +"http://www.yso.fi/onto/yso/p108498":"POINT(22.56528 61.64759)", +"http://www.yso.fi/onto/yso/p108500":"POINT(25.01247 64.24058)", +"http://www.yso.fi/onto/yso/p108501":"POINT(28.81371 63.68031)", +"http://www.yso.fi/onto/yso/p108504":"POINT(23.55545 60.45752)", +"http://www.yso.fi/onto/yso/p108505":"POINT(23.84679 61.67359)", +"http://www.yso.fi/onto/yso/p108512":"POINT(29.58364 61.65007)", +"http://www.yso.fi/onto/yso/p108513":"POINT(26.19598 60.62368)", +"http://www.yso.fi/onto/yso/p108514":"POINT(26.5284 64.94511)", +"http://www.yso.fi/onto/yso/p108515":"POINT(54 23.783333)", +"http://www.yso.fi/onto/yso/p108516":"POINT(55.309444444444 25.269722222222)", +"http://www.yso.fi/onto/yso/p108517":"POINT(54.368611111111 24.478055555556)", +"http://www.yso.fi/onto/yso/p108520":"POINT(27.73415 67.81503)", +"http://www.yso.fi/onto/yso/p108529":"POINT(22.54549 61.27914)", +"http://www.yso.fi/onto/yso/p108534":"POINT(24.13289 61.0783)", +"http://www.yso.fi/onto/yso/p108536":"POINT(22.55335 63.34947)", +"http://www.yso.fi/onto/yso/p108537":"POINT(27.66763 64.4056)", +"http://www.yso.fi/onto/yso/p108538":"POINT(22.35628 60.44818)", +"http://www.yso.fi/onto/yso/p108539":"POINT(26.04642 62.65736)", +"http://www.yso.fi/onto/yso/p108540":"POINT(24.48193 60.68947)", +"http://www.yso.fi/onto/yso/p108544":"POINT(23.73786 64.19816)", +"http://www.yso.fi/onto/yso/p108545":"POINT(29.611111111111 60.247222222222)", +"http://www.yso.fi/onto/yso/p108546":"POINT(25.44707 60.35769)", +"http://www.yso.fi/onto/yso/p108569":"POINT(22.3527 62.97502)", +"http://www.yso.fi/onto/yso/p108570":"POINT(23.06989 63.82361)", +"http://www.yso.fi/onto/yso/p108571":"POINT(25.00515 60.18572)", +"http://www.yso.fi/onto/yso/p108572":"POINT(24.59894 60.19144)", +"http://www.yso.fi/onto/yso/p108573":"POINT(24.50582 65.85892)", +"http://www.yso.fi/onto/yso/p108574":"POINT(25.07382 60.23832)", +"http://www.yso.fi/onto/yso/p108575":"POINT(25.11497 60.15974)", +"http://www.yso.fi/onto/yso/p108576":"POINT(24.50582 65.85892)", +"http://www.yso.fi/onto/yso/p108578":"POINT(29.873888888889 -1.9402777777778)", +"http://www.yso.fi/onto/yso/p108587":"POINT(25.033333333333 60.3)", +"http://www.yso.fi/onto/yso/p108594":"POINT(23.30274 61.34359)", +"http://www.yso.fi/onto/yso/p108595":"POINT(29.04505 62.42873)", +"http://www.yso.fi/onto/yso/p108596":"POINT(28.95992 62.42279)", +"http://www.yso.fi/onto/yso/p108597":"POINT(28.19384 62.58939)", +"http://www.yso.fi/onto/yso/p108598":"POINT(18.736111111111 59.387777777778)", +"http://www.yso.fi/onto/yso/p108600":"POINT(27.45854 60.34889)", +"http://www.yso.fi/onto/yso/p108601":"POINT(27.65 60.2)", +"http://www.yso.fi/onto/yso/p108604":"POINT(24.42989 60.16385)", +"http://www.yso.fi/onto/yso/p108644":"POINT(29.97531 62.73912)", +"http://www.yso.fi/onto/yso/p108645":"POINT(30.10445 62.70135)", +"http://www.yso.fi/onto/yso/p108646":"", +"http://www.yso.fi/onto/yso/p108647":"POINT(-64.991228611111 -17.056869611111)", +"http://www.yso.fi/onto/yso/p108648":"POINT(-78.816666666667 -1.4666666666667)", +"http://www.yso.fi/onto/yso/p108649":"POINT(-58.083333 -23.266667)", +"http://www.yso.fi/onto/yso/p108650":"POINT(-56.65 -32.733333)", +"http://www.yso.fi/onto/yso/p108651":"POINT(4.4791666666667 51.930833333333)", +"http://www.yso.fi/onto/yso/p108652":"POINT(27.0094 61.74103)", +"http://www.yso.fi/onto/yso/p108653":"POINT(25.06368 64.00805)", +"http://www.yso.fi/onto/yso/p108655":"POINT(23.76223 62.75211)", +"http://www.yso.fi/onto/yso/p108657":"POINT(23.68147 61.25898)", +"http://www.yso.fi/onto/yso/p108660":"POINT(-77.666667 23.916667)", +"http://www.yso.fi/onto/yso/p108661":"POINT(-79.033333333333 21.983333333333)", +"http://www.yso.fi/onto/yso/p108676":"POINT(29.86474 64.4521)", +"http://www.yso.fi/onto/yso/p108677":"POINT(28.55029 63.58183)", +"http://www.yso.fi/onto/yso/p108680":"POINT(23.73641 63.5193)", +"http://www.yso.fi/onto/yso/p108681":"POINT(33.95 35.116666666667)", +"http://www.yso.fi/onto/yso/p108682":"POINT(23.95534 59.92553)", +"http://www.yso.fi/onto/yso/p108683":"POINT(22.30391 60.30283)", +"http://www.yso.fi/onto/yso/p108702":"POINT(-7.8833333333333 42.75)", +"http://www.yso.fi/onto/yso/p108709":"POINT(27.12524 61.96798)", +"http://www.yso.fi/onto/yso/p108714":"POINT(21.66195 61.53948)", +"http://www.yso.fi/onto/yso/p108720":"", +"http://www.yso.fi/onto/yso/p108721":"", +"http://www.yso.fi/onto/yso/p108727":"POINT(24.6514 60.9209)", +"http://www.yso.fi/onto/yso/p108728":"POINT(27.80345 62.54926)", +"http://www.yso.fi/onto/yso/p108729":"POINT(27.9222 62.60254)", +"http://www.yso.fi/onto/yso/p108730":"POINT(27.88646 62.53999)", +"http://www.yso.fi/onto/yso/p108731":"POINT(27.8875 62.57702)", +"http://www.yso.fi/onto/yso/p108733":"POINT(24.85388 60.30019)", +"http://www.yso.fi/onto/yso/p108735":"POINT(25.24083 61.81708)", +"http://www.yso.fi/onto/yso/p108736":"POINT(24.77043 60.17727)", +"http://www.yso.fi/onto/yso/p108739":"POINT(45.7167 43.4)", +"http://www.yso.fi/onto/yso/p108740":"POINT(45.68333333 43.31666667)", +"http://www.yso.fi/onto/yso/p108741":"POINT(44.966666666667 43.2)", +"http://www.yso.fi/onto/yso/p108743":"POINT(121.46666666667 31.166666666667)", +"http://www.yso.fi/onto/yso/p108744":"POINT(108.9 34.266666666667)", +"http://www.yso.fi/onto/yso/p108747":"POINT(22.29817 60.6129)", +"http://www.yso.fi/onto/yso/p108748":"POINT(22.58899 60.64659)", +"http://www.yso.fi/onto/yso/p108749":"POINT(21.97695 60.55675)", +"http://www.yso.fi/onto/yso/p108763":"POINT(25.67538 62.44423)", +"http://www.yso.fi/onto/yso/p108764":"POINT(24.83284 60.18794)", +"http://www.yso.fi/onto/yso/p108765":"POINT(23.13095 61.23188)", +"http://www.yso.fi/onto/yso/p108771":"POINT(21.54784 63.03498)", +"http://www.yso.fi/onto/yso/p108772":"POINT(21.54784 63.03498)", +"http://www.yso.fi/onto/yso/p108773":"", +"http://www.yso.fi/onto/yso/p108774":"", +"http://www.yso.fi/onto/yso/p108775":"", +"http://www.yso.fi/onto/yso/p108776":"POINT(26.21001 60.58408)", +"http://www.yso.fi/onto/yso/p108777":"POINT(21.48982 62.46412)", +"http://www.yso.fi/onto/yso/p108780":"", +"http://www.yso.fi/onto/yso/p108808":"POINT(26.85744 68.59852)", +"http://www.yso.fi/onto/yso/p108811":"POINT(28.75892 65.4854)", +"http://www.yso.fi/onto/yso/p108812":"POINT(23.15201 60.93973)", +"http://www.yso.fi/onto/yso/p108813":"POINT(22.66356 60.04845)", +"http://www.yso.fi/onto/yso/p108814":"POINT(22.97746658 62.46196747)", +"http://www.yso.fi/onto/yso/p108815":"POINT(21.9047 62.99402)", +"http://www.yso.fi/onto/yso/p108816":"POINT(23.81795 63.37595)", +"http://www.yso.fi/onto/yso/p108817":"POINT(22.88809 63.34375)", +"http://www.yso.fi/onto/yso/p108818":"POINT(23.6231 63.02587)", +"http://www.yso.fi/onto/yso/p108820":"POINT(24.975 60.204166666667)", +"http://www.yso.fi/onto/yso/p108821":"POINT(24.56288 61.57756)", +"http://www.yso.fi/onto/yso/p108825":"POINT(27.69543 64.95708)", +"http://www.yso.fi/onto/yso/p108826":"POINT(29.37282 64.96188)", +"http://www.yso.fi/onto/yso/p108827":"POINT(29.40775 64.97247)", +"http://www.yso.fi/onto/yso/p108829":"POINT(38.583333 35.216667)", +"http://www.yso.fi/onto/yso/p108837":"POINT(24.27672 60.02659)", +"http://www.yso.fi/onto/yso/p108838":"POINT(27.82642 62.17951)", +"http://www.yso.fi/onto/yso/p108839":"", +"http://www.yso.fi/onto/yso/p108840":"POINT(-157.79638888889 21.311388888889)", +"http://www.yso.fi/onto/yso/p108844":"POINT(27.24959 60.96102)", +"http://www.yso.fi/onto/yso/p108845":"POINT(27.20287 60.96746)", +"http://www.yso.fi/onto/yso/p108846":"POINT(14.25 40.833333333333)", +"http://www.yso.fi/onto/yso/p108847":"POINT(23.06072 59.83768)", +"http://www.yso.fi/onto/yso/p108868":"", +"http://www.yso.fi/onto/yso/p108881":"POINT(-79.4 43.7)", +"http://www.yso.fi/onto/yso/p108882":"POINT(30.9375 61.80388889)", +"http://www.yso.fi/onto/yso/p108886":"POINT(27.96954 64.20251)", +"http://www.yso.fi/onto/yso/p108887":"POINT(28.00491 64.19503)", +"http://www.yso.fi/onto/yso/p108889":"POINT(26.97082 60.48226)", +"http://www.yso.fi/onto/yso/p108896":"POINT(27.6556 62.91016)", +"http://www.yso.fi/onto/yso/p108897":"POINT(31.239444444444 30.056111111111)", +"http://www.yso.fi/onto/yso/p108900":"POINT(24.67558 60.17643)", +"http://www.yso.fi/onto/yso/p108901":"POINT(2 28)", +"http://www.yso.fi/onto/yso/p108909":"", +"http://www.yso.fi/onto/yso/p108958":"POINT(26.49085 61.0158)", +"http://www.yso.fi/onto/yso/p108959":"POINT(24.06658 66.42707)", +"http://www.yso.fi/onto/yso/p108960":"POINT(25.03446 62.64249)", +"http://www.yso.fi/onto/yso/p108962":"", +"http://www.yso.fi/onto/yso/p108963":"POINT(29.04986 62.10699)", +"http://www.yso.fi/onto/yso/p108964":"POINT(29.07708 62.08228)", +"http://www.yso.fi/onto/yso/p108965":"POINT(29.04164 62.05192)", +"http://www.yso.fi/onto/yso/p108971":"POINT(23.45484 61.49553)", +"http://www.yso.fi/onto/yso/p108979":"POINT(24.86808 60.22326)", +"http://www.yso.fi/onto/yso/p108982":"POINT(25.74898 63.36009)", +"http://www.yso.fi/onto/yso/p108983":"POINT(29.48051 64.24674)", +"http://www.yso.fi/onto/yso/p108984":"POINT(24.82643 60.27848)", +"http://www.yso.fi/onto/yso/p108986":"POINT(27.00923 60.43128)", +"http://www.yso.fi/onto/yso/p108987":"POINT(22.06113 62.35954)", +"http://www.yso.fi/onto/yso/p108988":"POINT(22.753372 60.70276)", +"http://www.yso.fi/onto/yso/p108990":"POINT(19.94143 60.11356)", +"http://www.yso.fi/onto/yso/p108991":"POINT(22.10825 63.05432)", +"http://www.yso.fi/onto/yso/p108992":"POINT(22.25057 60.43253)", +"http://www.yso.fi/onto/yso/p108994":"POINT(26.67354 63.26689)", +"http://www.yso.fi/onto/yso/p108995":"POINT(26.71152 63.30415)", +"http://www.yso.fi/onto/yso/p108996":"POINT(25.10917 60.53271)", +"http://www.yso.fi/onto/yso/p109005":"POINT(25.78346 63.89363)", +"http://www.yso.fi/onto/yso/p109006":"POINT(28.05758 61.16133)", +"http://www.yso.fi/onto/yso/p109010":"POINT(24.6711 62.01825)", +"http://www.yso.fi/onto/yso/p109012":"POINT(96 22)", +"http://www.yso.fi/onto/yso/p109015":"POINT(21.65538 60.1019)", +"http://www.yso.fi/onto/yso/p109016":"POINT(25.14718 60.25421)", +"http://www.yso.fi/onto/yso/p109024":"POINT(27.20384 63.2201)", +"http://www.yso.fi/onto/yso/p109025":"POINT(-13 25)", +"http://www.yso.fi/onto/yso/p109027":"POINT(29.23504 62.64714)", +"http://www.yso.fi/onto/yso/p109037":"POINT(29.65714 61.55261)", +"http://www.yso.fi/onto/yso/p109039":"POINT(22.583333333333 58.866666666667)", +"http://www.yso.fi/onto/yso/p109040":"POINT(21.82106 61.50372)", +"http://www.yso.fi/onto/yso/p109041":"POINT(22.71977 63.72708)", +"http://www.yso.fi/onto/yso/p109042":"POINT(22.99949 63.62002)", +"http://www.yso.fi/onto/yso/p109045":"POINT(25.46794 65.05701)", +"http://www.yso.fi/onto/yso/p109046":"POINT(25.03846 60.78817)", +"http://www.yso.fi/onto/yso/p109047":"POINT(26.69693 67.28941)", +"http://www.yso.fi/onto/yso/p109050":"POINT(24.92928 60.16844)", +"http://www.yso.fi/onto/yso/p109051":"POINT(24.93516 60.16175)", +"http://www.yso.fi/onto/yso/p109053":"POINT(27.78976 67.07924)", +"http://www.yso.fi/onto/yso/p109054":"POINT(29.1294 65.089)", +"http://www.yso.fi/onto/yso/p109057":"POINT(29.23904 64.79262)", +"http://www.yso.fi/onto/yso/p109074":"POINT(21.23094 62.57884)", +"http://www.yso.fi/onto/yso/p109075":"POINT(24.75692 60.56375)", +"http://www.yso.fi/onto/yso/p109077":"POINT(24.14545 62.55074)", +"http://www.yso.fi/onto/yso/p109078":"POINT(23.9146 63.06634)", +"http://www.yso.fi/onto/yso/p109083":"POINT(20.87574 59.94642)", +"http://www.yso.fi/onto/yso/p109084":"POINT(29.44803 61.46777)", +"http://www.yso.fi/onto/yso/p109088":"", +"http://www.yso.fi/onto/yso/p109090":"POINT(30.11093 62.34806)", +"http://www.yso.fi/onto/yso/p109093":"POINT(0 -90)", +"http://www.yso.fi/onto/yso/p109095":"", +"http://www.yso.fi/onto/yso/p109095":"", +"http://www.yso.fi/onto/yso/p109128":"POINT(17.545 59.336666666667)", +"http://www.yso.fi/onto/yso/p109135":"POINT(25.36085 61.60231)", +"http://www.yso.fi/onto/yso/p109138":"POINT(25.01576 60.10264)", +"http://www.yso.fi/onto/yso/p109146":"POINT(30.97779 62.77372)", +"http://www.yso.fi/onto/yso/p109148":"POINT(-161 -15)", +"http://www.yso.fi/onto/yso/p109149":"POINT(28.20215 63.0147)", +"http://www.yso.fi/onto/yso/p109150":"POINT(30.03617 61.90197)", +"http://www.yso.fi/onto/yso/p109151":"POINT(29.28647 62.5545)", +"http://www.yso.fi/onto/yso/p109152":"POINT(27.1244 63.54797)", +"http://www.yso.fi/onto/yso/p109153":"POINT(23.19579 62.14803)", +"http://www.yso.fi/onto/yso/p109154":"POINT(27.8856 62.41866)", +"http://www.yso.fi/onto/yso/p109155":"POINT(27.7 62.6)", +"http://www.yso.fi/onto/yso/p109156":"POINT(27.73862 62.55504)", +"http://www.yso.fi/onto/yso/p109157":"POINT(24.04176 62.70359)", +"http://www.yso.fi/onto/yso/p109158":"POINT(25.54885 61.17257)", +"http://www.yso.fi/onto/yso/p109159":"POINT(29.75362 63.05708)", +"http://www.yso.fi/onto/yso/p109160":"POINT(24.95907 60.35849)", +"http://www.yso.fi/onto/yso/p109161":"", +"http://www.yso.fi/onto/yso/p109161":"", +"http://www.yso.fi/onto/yso/p109162":"POINT(23.62624 61.52052)", +"http://www.yso.fi/onto/yso/p109163":"POINT(26.88596 67.14551)", +"http://www.yso.fi/onto/yso/p109164":"POINT(26.24501 62.41096)", +"http://www.yso.fi/onto/yso/p109165":"POINT(25.51545 61.00885)", +"http://www.yso.fi/onto/yso/p109166":"POINT(47 -20)", +"http://www.yso.fi/onto/yso/p109167":"POINT(73.22 3.2)", +"http://www.yso.fi/onto/yso/p109168":"POINT(171.06666666667 7.1166666666667)", +"http://www.yso.fi/onto/yso/p109169":"POINT(-10.65 19.566667)", +"http://www.yso.fi/onto/yso/p109170":"POINT(26.15111 61.61477)", +"http://www.yso.fi/onto/yso/p109171":"POINT(26.2597 61.74613)", +"http://www.yso.fi/onto/yso/p109172":"", +"http://www.yso.fi/onto/yso/p109173":"POINT(166.93333333333 -0.53333333333333)", +"http://www.yso.fi/onto/yso/p109174":"POINT(-6 8)", +"http://www.yso.fi/onto/yso/p109175":"POINT(55.966667 20.233333)", +"http://www.yso.fi/onto/yso/p109176":"POINT(134.55 7.466667)", +"http://www.yso.fi/onto/yso/p109177":"POINT(-80.366667 8.616667)", +"http://www.yso.fi/onto/yso/p109178":"POINT(146 17)", +"http://www.yso.fi/onto/yso/p109182":"POINT(-99.145555555556 19.419444444444)", +"http://www.yso.fi/onto/yso/p109184":"POINT(-159.78333333333 -21.233333333333)", +"http://www.yso.fi/onto/yso/p109185":"POINT(25.72952 63.30445)", +"http://www.yso.fi/onto/yso/p109186":"POINT(28.00467 66.55318)", +"http://www.yso.fi/onto/yso/p109187":"POINT(23.94695 63.22107)", +"http://www.yso.fi/onto/yso/p109188":"POINT(24.4018 60.14921)", +"http://www.yso.fi/onto/yso/p109189":"POINT(22.6066 60.72077)", +"http://www.yso.fi/onto/yso/p109190":"POINT(27.35017 63.32342)", +"http://www.yso.fi/onto/yso/p109191":"POINT(27.26641 63.16053)", +"http://www.yso.fi/onto/yso/p109192":"POINT(26.50511 63.11725)", +"http://www.yso.fi/onto/yso/p109193":"POINT(24.05963 62.12388)", +"http://www.yso.fi/onto/yso/p109194":"POINT(26.96955 62.82467)", +"http://www.yso.fi/onto/yso/p109195":"POINT(10 1.5)", +"http://www.yso.fi/onto/yso/p109196":"POINT(51.212767 25.269535)", +"http://www.yso.fi/onto/yso/p109197":"POINT(-62.666667 17.33)", +"http://www.yso.fi/onto/yso/p109198":"POINT(-61 13.883333)", +"http://www.yso.fi/onto/yso/p109199":"POINT(-61.233333333333 13.166666666667)", +"http://www.yso.fi/onto/yso/p109200":"POINT(159.81666666667 -9.4666666666667)", +"http://www.yso.fi/onto/yso/p109201":"POINT(-172 -14)", +"http://www.yso.fi/onto/yso/p109202":"POINT(6.6 0.316667)", +"http://www.yso.fi/onto/yso/p109203":"POINT(52.766667 -7.1)", +"http://www.yso.fi/onto/yso/p109204":"POINT(-11.916667 8.5)", +"http://www.yso.fi/onto/yso/p109205":"POINT(17.95 66.033333333333)", +"http://www.yso.fi/onto/yso/p109206":"POINT(20.75 67.116666666667)", +"http://www.yso.fi/onto/yso/p109207":"POINT(18.666666666667 64.6)", +"http://www.yso.fi/onto/yso/p109208":"POINT(20.25 63.833333333333)", +"http://www.yso.fi/onto/yso/p109209":"POINT(4.3175 52.084166666667)", +"http://www.yso.fi/onto/yso/p109210":"POINT(-56 4)", +"http://www.yso.fi/onto/yso/p109211":"POINT(-61.85 17.116666666667)", +"http://www.yso.fi/onto/yso/p109212":"POINT(1.183333 8.25)", +"http://www.yso.fi/onto/yso/p109213":"POINT(-174.810278 -20.587778)", +"http://www.yso.fi/onto/yso/p109214":"POINT(-61.516666666667 10.666666666667)", +"http://www.yso.fi/onto/yso/p109215":"POINT(19.4 15.466667)", +"http://www.yso.fi/onto/yso/p109216":"POINT(178.005556 -7.475)", +"http://www.yso.fi/onto/yso/p109217":"POINT(50.551111 26.0675)", +"http://www.yso.fi/onto/yso/p109218":"POINT(-59.55 13.166666666667)", +"http://www.yso.fi/onto/yso/p109219":"POINT(168.01666944444 -16.633330555556)", +"http://www.yso.fi/onto/yso/p109220":"POINT(23.89225 61.54537)", +"http://www.yso.fi/onto/yso/p109222":"", +"http://www.yso.fi/onto/yso/p109223":"POINT(6.15 46.2)", +"http://www.yso.fi/onto/yso/p109224":"POINT(-88.7 17.066666666667)", +"http://www.yso.fi/onto/yso/p109225":"POINT(2.183333 8.833333)", +"http://www.yso.fi/onto/yso/p109226":"POINT(-88.7 17.066666666667)", +"http://www.yso.fi/onto/yso/p109227":"POINT(1.9880555555556 7.1855555555556)", +"http://www.yso.fi/onto/yso/p109229":"POINT(20 41)", +"http://www.yso.fi/onto/yso/p109230":"POINT(29.816667 -3.666667)", +"http://www.yso.fi/onto/yso/p109231":"POINT(42.433333 11.8)", +"http://www.yso.fi/onto/yso/p109232":"POINT(-61.333333 15.416667)", +"http://www.yso.fi/onto/yso/p109233":"POINT(-70.666667 19)", +"http://www.yso.fi/onto/yso/p109234":"POINT(38.25 15.483333)", +"http://www.yso.fi/onto/yso/p109235":"POINT(32.899730555556 24.088919444444)", +"http://www.yso.fi/onto/yso/p109236":"POINT(31.51 30.572777777778)", +"http://www.yso.fi/onto/yso/p109237":"POINT(32.533333333333 29.966666666667)", +"http://www.yso.fi/onto/yso/p109243":"POINT(22.66706 60.44475)", +"http://www.yso.fi/onto/yso/p109251":"POINT(24.71307 65.82089)", +"http://www.yso.fi/onto/yso/p109268":"POINT(28.09377 64.33716)", +"http://www.yso.fi/onto/yso/p109269":"POINT(24.36921 61.08967)", +"http://www.yso.fi/onto/yso/p109271":"POINT(18.183333333333 63.016666666667)", +"http://www.yso.fi/onto/yso/p109274":"POINT(11.431111111111 48.7775)", +"http://www.yso.fi/onto/yso/p109276":"POINT(24.96294 60.55412)", +"http://www.yso.fi/onto/yso/p109277":"POINT(25.09091 60.38403)", +"http://www.yso.fi/onto/yso/p109278":"POINT(32.36138889 64.79722222)", +"http://www.yso.fi/onto/yso/p109279":"POINT(32.1 64.75)", +"http://www.yso.fi/onto/yso/p109282":"POINT(178 -18)", +"http://www.yso.fi/onto/yso/p109283":"POINT(-3.6919444444444 40.418888888889)", +"http://www.yso.fi/onto/yso/p109284":"POINT(-4.8834416666667 36.511363888889)", +"http://www.yso.fi/onto/yso/p109285":"POINT(-4.4996349 36.624537)", +"http://www.yso.fi/onto/yso/p109286":"POINT(39 8)", +"http://www.yso.fi/onto/yso/p109287":"POINT(-15.431388888889 28.127222222222)", +"http://www.yso.fi/onto/yso/p109289":"POINT(11.5 -0.68333055555556)", +"http://www.yso.fi/onto/yso/p109290":"POINT(-15.5 13.5)", +"http://www.yso.fi/onto/yso/p109291":"POINT(-61.666667 12.116667)", +"http://www.yso.fi/onto/yso/p109292":"POINT(-15 12)", +"http://www.yso.fi/onto/yso/p109293":"POINT(-59.316667 5.733333)", +"http://www.yso.fi/onto/yso/p109294":"POINT(-72.33 19.1)", +"http://www.yso.fi/onto/yso/p109296":"POINT(15.593819444444 40.679763888889)", +"http://www.yso.fi/onto/yso/p109297":"POINT(-5.9383333333333 54.596666666667)", +"http://www.yso.fi/onto/yso/p109298":"POINT(-24.083333333333 15.916666666667)", +"http://www.yso.fi/onto/yso/p109299":"POINT(20.616667 6.966667)", +"http://www.yso.fi/onto/yso/p109300":"POINT(173.03333333333 1.4666666666667)", +"http://www.yso.fi/onto/yso/p109301":"POINT(44.233333 -12.183333)", +"http://www.yso.fi/onto/yso/p109302":"POINT(127.23333333333 38.316666666667)", +"http://www.yso.fi/onto/yso/p109303":"POINT(-114.06666666667 51.05)", +"http://www.yso.fi/onto/yso/p109304":"POINT(-106 55)", +"http://www.yso.fi/onto/yso/p109307":"", +"http://www.yso.fi/onto/yso/p109308":"POINT(25.86608 64.27064)", +"http://www.yso.fi/onto/yso/p109309":"POINT(25.34543 66.24676)", +"http://www.yso.fi/onto/yso/p109312":"POINT(23.56333 59.82526)", +"http://www.yso.fi/onto/yso/p109313":"", +"http://www.yso.fi/onto/yso/p109314":"POINT(13.820833333333 55.430555555556)", +"http://www.yso.fi/onto/yso/p109316":"", +"http://www.yso.fi/onto/yso/p109321":"POINT(22 63)", +"http://www.yso.fi/onto/yso/p109329":"POINT(47.6 29.166667)", +"http://www.yso.fi/onto/yso/p109330":"POINT(-9.75 6.533333)", +"http://www.yso.fi/onto/yso/p109331":"POINT(17 27)", +"http://www.yso.fi/onto/yso/p109336":"", +"http://www.yso.fi/onto/yso/p109337":"POINT(21.56597 60.81812)", +"http://www.yso.fi/onto/yso/p109339":"POINT(30.194 62.09677)", +"http://www.yso.fi/onto/yso/p109343":"POINT(26.5668 61.16162)", +"http://www.yso.fi/onto/yso/p109344":"POINT(26.61989 61.17466)", +"http://www.yso.fi/onto/yso/p109348":"POINT(24.76398 60.25417)", +"http://www.yso.fi/onto/yso/p109350":"POINT(30.406944444444 59.944166666667)", +"http://www.yso.fi/onto/yso/p109377":"POINT(24.29864 67.52464)", +"http://www.yso.fi/onto/yso/p109378":"POINT(22.21124 60.36546)", +"http://www.yso.fi/onto/yso/p109380":"POINT(29.10466 69.6652)", +"http://www.yso.fi/onto/yso/p109383":"POINT(30.76031 62.37363)", +"http://www.yso.fi/onto/yso/p109384":"POINT(23.67697 61.51292)", +"http://www.yso.fi/onto/yso/p109385":"POINT(23.88493 61.46752)", +"http://www.yso.fi/onto/yso/p109388":"POINT(25.07233 61.84694)", +"http://www.yso.fi/onto/yso/p109389":"POINT(24.38215 60.29299)", +"http://www.yso.fi/onto/yso/p109390":"POINT(22.24647 60.44455)", +"http://www.yso.fi/onto/yso/p109392":"POINT(-3.18 51.48)", +"http://www.yso.fi/onto/yso/p109402":"", +"http://www.yso.fi/onto/yso/p109403":"POINT(29.21608 61.29762)", +"http://www.yso.fi/onto/yso/p109404":"POINT(20.98572 60.31578)", +"http://www.yso.fi/onto/yso/p109406":"POINT(27.12693 68.10768)", +"http://www.yso.fi/onto/yso/p109407":"POINT(27.72337 60.64311)", +"http://www.yso.fi/onto/yso/p109411":"POINT(33.55 60.733333333333)", +"http://www.yso.fi/onto/yso/p109415":"POINT(28.65682 62.70413)", +"http://www.yso.fi/onto/yso/p109416":"POINT(30.59032 62.17808)", +"http://www.yso.fi/onto/yso/p109452":"POINT(24.79444444 60.16833333)", +"http://www.yso.fi/onto/yso/p109454":"POINT(29.64168 61.85019)", +"http://www.yso.fi/onto/yso/p109455":"POINT(27.2 59.85)", +"http://www.yso.fi/onto/yso/p109456":"POINT(27.848611111111 60.01)", +"http://www.yso.fi/onto/yso/p109457":"POINT(25.99385 61.5116)", +"http://www.yso.fi/onto/yso/p109458":"POINT(23.18066 61.16742)", +"http://www.yso.fi/onto/yso/p109459":"POINT(26.96354 61.65215)", +"http://www.yso.fi/onto/yso/p109460":"POINT(22.78725 62.81122)", +"http://www.yso.fi/onto/yso/p109461":"POINT(29.88462 62.47755)", +"http://www.yso.fi/onto/yso/p109462":"POINT(28.677 60.6442)", +"http://www.yso.fi/onto/yso/p109463":"POINT(28.17997 61.52186)", +"http://www.yso.fi/onto/yso/p109464":"POINT(25.87763 60.26865)", +"http://www.yso.fi/onto/yso/p109465":"POINT(26.77897 62.67562)", +"http://www.yso.fi/onto/yso/p109466":"POINT(26.83329 62.66934)", +"http://www.yso.fi/onto/yso/p109467":"POINT(23.98499 62.4795)", +"http://www.yso.fi/onto/yso/p109469":"POINT(30.39112 62.42341)", +"http://www.yso.fi/onto/yso/p109470":"POINT(31.8315 65.885480555556)", +"http://www.yso.fi/onto/yso/p109475":"POINT(29.2783 62.74397)", +"http://www.yso.fi/onto/yso/p109476":"POINT(25.15475 61.02188)", +"http://www.yso.fi/onto/yso/p109485":"POINT(22.31246 60.46679)", +"http://www.yso.fi/onto/yso/p109487":"POINT(26.12838 62.14895)", +"http://www.yso.fi/onto/yso/p109488":"POINT(28.90158 61.7019)", +"http://www.yso.fi/onto/yso/p109489":"POINT(54.466666666667 64.283333333333)", +"http://www.yso.fi/onto/yso/p109492":"POINT(30.523611111111 50.45)", +"http://www.yso.fi/onto/yso/p109500":"POINT(24.66653 60.13766)", +"http://www.yso.fi/onto/yso/p109501":"POINT(25.24264 63.54132)", +"http://www.yso.fi/onto/yso/p109502":"POINT(22.87956 62.55659)", +"http://www.yso.fi/onto/yso/p109503":"POINT(24.14627 67.21746)", +"http://www.yso.fi/onto/yso/p109504":"POINT(24.6361 60.15379)", +"http://www.yso.fi/onto/yso/p109505":"POINT(28.7841 61.17624)", +"http://www.yso.fi/onto/yso/p109507":"POINT(22.38265 63.48726)", +"http://www.yso.fi/onto/yso/p109508":"POINT(24.49341 60.45991)", +"http://www.yso.fi/onto/yso/p109509":"POINT(23.72195 61.49566)", +"http://www.yso.fi/onto/yso/p109510":"POINT(23.19622 63.81661)", +"http://www.yso.fi/onto/yso/p109512":"", +"http://www.yso.fi/onto/yso/p109534":"POINT(33.013333333333 68.889166666667)", +"http://www.yso.fi/onto/yso/p109548":"POINT(24.94555556 60.16944444)", +"http://www.yso.fi/onto/yso/p109553":"POINT(28.80346 61.11557)", +"http://www.yso.fi/onto/yso/p109555":"POINT(24.99985 60.20766)", +"http://www.yso.fi/onto/yso/p109556":"POINT(24.99434 60.20397)", +"http://www.yso.fi/onto/yso/p109560":"POINT(23.74145 62.60432)", +"http://www.yso.fi/onto/yso/p109562":"POINT(22.86616 63.57121)", +"http://www.yso.fi/onto/yso/p109563":"POINT(-4.27 55.87)", +"http://www.yso.fi/onto/yso/p109564":"", +"http://www.yso.fi/onto/yso/p109571":"POINT(29.39116 67.96249)", +"http://www.yso.fi/onto/yso/p109572":"POINT(21.62633 61.67802)", +"http://www.yso.fi/onto/yso/p109573":"POINT(21.5681 61.6933)", +"http://www.yso.fi/onto/yso/p109574":"POINT(23.53685 60.12284)", +"http://www.yso.fi/onto/yso/p109575":"POINT(21.51066 60.67951)", +"http://www.yso.fi/onto/yso/p109580":"POINT(23.4325 63.55412)", +"http://www.yso.fi/onto/yso/p109581":"POINT(23.4325 63.55412)", +"http://www.yso.fi/onto/yso/p109582":"POINT(23.74287 62.9821)", +"http://www.yso.fi/onto/yso/p109583":"POINT(23.82619 64.11475)", +"http://www.yso.fi/onto/yso/p109584":"POINT(21.45382 61.1362)", +"http://www.yso.fi/onto/yso/p109586":"POINT(24.86797 60.15897)", +"http://www.yso.fi/onto/yso/p109587":"POINT(25.8458 65.37181)", +"http://www.yso.fi/onto/yso/p109589":"POINT(13 55.605555555556)", +"http://www.yso.fi/onto/yso/p109590":"POINT(17.633333333333 59.183333333333)", +"http://www.yso.fi/onto/yso/p109594":"POINT(24.95733 60.151)", +"http://www.yso.fi/onto/yso/p109619":"POINT(25.70945 61.93383)", +"http://www.yso.fi/onto/yso/p109620":"POINT(27.10016 68.18064)", +"http://www.yso.fi/onto/yso/p109624":"POINT(54.66666667 59.01666667)", +"http://www.yso.fi/onto/yso/p109628":"POINT(22.91962 62.85551)", +"http://www.yso.fi/onto/yso/p109629":"POINT(24.03036 60.23276)", +"http://www.yso.fi/onto/yso/p109630":"POINT(24.52041 60.84839)", +"http://www.yso.fi/onto/yso/p109631":"POINT(24.923888888889 60.179166666667)", +"http://www.yso.fi/onto/yso/p109632":"POINT(24.93405 60.18082)", +"http://www.yso.fi/onto/yso/p109633":"POINT(24.39623 60.1201)", +"http://www.yso.fi/onto/yso/p109638":"", +"http://www.yso.fi/onto/yso/p109639":"POINT(24.92876 61.24692)", +"http://www.yso.fi/onto/yso/p109640":"POINT(24.91286 61.16843)", +"http://www.yso.fi/onto/yso/p109641":"POINT(24.90674 61.01678)", +"http://www.yso.fi/onto/yso/p109647":"POINT(12.75 55.75)", +"http://www.yso.fi/onto/yso/p109648":"POINT(23.584722222222 57.625555555556)", +"http://www.yso.fi/onto/yso/p109649":"POINT(27.52238 61.79573)", +"http://www.yso.fi/onto/yso/p109650":"POINT(21.50346 60.84197)", +"http://www.yso.fi/onto/yso/p109651":"POINT(21.43672 60.80115)", +"http://www.yso.fi/onto/yso/p109656":"POINT(24.81595 60.26548)", +"http://www.yso.fi/onto/yso/p109658":"POINT(24.82882 65.38496)", +"http://www.yso.fi/onto/yso/p109659":"POINT(25.50042 65.02536)", +"http://www.yso.fi/onto/yso/p109660":"POINT(22.08081 60.84564)", +"http://www.yso.fi/onto/yso/p109661":"POINT(22.09397 62.42161)", +"http://www.yso.fi/onto/yso/p109662":"POINT(23.67545 62.13631)", +"http://www.yso.fi/onto/yso/p109663":"POINT(23.04636 60.11788)", +"http://www.yso.fi/onto/yso/p109664":"POINT(21.98363 60.4499)", +"http://www.yso.fi/onto/yso/p109665":"POINT(21.6042 61.56472)", +"http://www.yso.fi/onto/yso/p109667":"POINT(27.56265 66.69739)", +"http://www.yso.fi/onto/yso/p109669":"POINT(22.80838 61.3895)", +"http://www.yso.fi/onto/yso/p109678":"POINT(28.516666666667 60.583333333333)", +"http://www.yso.fi/onto/yso/p109679":"", +"http://www.yso.fi/onto/yso/p109724":"POINT(27.27737 62.57074)", +"http://www.yso.fi/onto/yso/p109725":"POINT(27.34024 62.51359)", +"http://www.yso.fi/onto/yso/p109728":"POINT(23.97746 60.36694)", +"http://www.yso.fi/onto/yso/p109729":"POINT(24.99297 63.89302)", +"http://www.yso.fi/onto/yso/p109730":"POINT(28.10631 62.54556)", +"http://www.yso.fi/onto/yso/p109743":"POINT(26.48292 64.5749)", +"http://www.yso.fi/onto/yso/p109746":"POINT(27.46161 61.45433)", +"http://www.yso.fi/onto/yso/p109748":"POINT(26.71807 63.36495)", +"http://www.yso.fi/onto/yso/p109749":"POINT(26.6221 63.3906)", +"http://www.yso.fi/onto/yso/p109750":"POINT(26.5828 63.34243)", +"http://www.yso.fi/onto/yso/p109751":"POINT(28.09482 62.59245)", +"http://www.yso.fi/onto/yso/p109752":"POINT(27.99755 62.56153)", +"http://www.yso.fi/onto/yso/p109766":"POINT(24.7255 67.78339)", +"http://www.yso.fi/onto/yso/p109767":"POINT(23.61893 68.38574)", +"http://www.yso.fi/onto/yso/p109768":"POINT(24.90441 64.30145)", +"http://www.yso.fi/onto/yso/p109769":"POINT(27.16942 60.60186)", +"http://www.yso.fi/onto/yso/p109770":"POINT(28.76984 61.20983)", +"http://www.yso.fi/onto/yso/p109771":"POINT(27.1099 60.56587)", +"http://www.yso.fi/onto/yso/p109772":"POINT(27.03888 60.56386)", +"http://www.yso.fi/onto/yso/p109773":"POINT(27.02789 60.54962)", +"http://www.yso.fi/onto/yso/p109774":"POINT(23.26633 62.88454)", +"http://www.yso.fi/onto/yso/p109775":"POINT(22.8478 63.03377)", +"http://www.yso.fi/onto/yso/p109776":"POINT(22.98439 62.9083)", +"http://www.yso.fi/onto/yso/p109777":"POINT(23.86161 60.14486)", +"http://www.yso.fi/onto/yso/p109778":"POINT(24.14585 67.60502)", +"http://www.yso.fi/onto/yso/p109779":"POINT(28.24872 64.74082)", +"http://www.yso.fi/onto/yso/p109780":"POINT(28.03247 64.66734)", +"http://www.yso.fi/onto/yso/p109783":"POINT(22.2262 60.05176)", +"http://www.yso.fi/onto/yso/p109785":"POINT(24.90553 60.22246)", +"http://www.yso.fi/onto/yso/p109786":"POINT(27.33855 60.74925)", +"http://www.yso.fi/onto/yso/p109845":"POINT(-75 15)", +"http://www.yso.fi/onto/yso/p109864":"POINT(18.633333333333 54.366666666667)", +"http://www.yso.fi/onto/yso/p109865":"POINT(-21.883333333333 64.15)", +"http://www.yso.fi/onto/yso/p109866":"POINT(25.20953 60.88976)", +"http://www.yso.fi/onto/yso/p109867":"POINT(24.68967 60.85269)", +"http://www.yso.fi/onto/yso/p109868":"POINT(26.68333333 67.98333333)", +"http://www.yso.fi/onto/yso/p109869":"POINT(24.76472 63.95012)", +"http://www.yso.fi/onto/yso/p109870":"POINT(24.71439 60.24529)", +"http://www.yso.fi/onto/yso/p109872":"POINT(24.68081 60.22464)", +"http://www.yso.fi/onto/yso/p109873":"POINT(24.66163 60.25834)", +"http://www.yso.fi/onto/yso/p109874":"POINT(24.74983 61.73893)", +"http://www.yso.fi/onto/yso/p109875":"POINT(24.68067 61.92153)", +"http://www.yso.fi/onto/yso/p109876":"POINT(24.77931 61.78525)", +"http://www.yso.fi/onto/yso/p109877":"POINT(24.80891 61.8054)", +"http://www.yso.fi/onto/yso/p109879":"POINT(23.2133 61.6361)", +"http://www.yso.fi/onto/yso/p109881":"POINT(28.23998 65.44628)", +"http://www.yso.fi/onto/yso/p109882":"POINT(28.4177 68.90034)", +"http://www.yso.fi/onto/yso/p109885":"POINT(22.10301 61.45356)", +"http://www.yso.fi/onto/yso/p109887":"POINT(32.966666666667 60.983333333333)", +"http://www.yso.fi/onto/yso/p109948":"POINT(24.81903 60.60163)", +"http://www.yso.fi/onto/yso/p109949":"POINT(26.22273 60.8463)", +"http://www.yso.fi/onto/yso/p109950":"POINT(29.04117 63.23804)", +"http://www.yso.fi/onto/yso/p109951":"POINT(28.74189 63.29483)", +"http://www.yso.fi/onto/yso/p109952":"POINT(28.7351 63.19907)", +"http://www.yso.fi/onto/yso/p109957":"", +"http://www.yso.fi/onto/yso/p109958":"POINT(26.19847 62.26163)", +"http://www.yso.fi/onto/yso/p109959":"POINT(32.782161111111 64.081663888889)", +"http://www.yso.fi/onto/yso/p109962":"POINT(28.050277777778 -26.145)", +"http://www.yso.fi/onto/yso/p109963":"POINT(-89.253333333333 29.151111111111)", +"http://www.yso.fi/onto/yso/p109970":"POINT(44.121111111111 42.261111111111)", +"http://www.yso.fi/onto/yso/p109975":"POINT(21.07711 60.51799)", +"http://www.yso.fi/onto/yso/p109979":"POINT(24.74156 60.1612)", +"http://www.yso.fi/onto/yso/p109980":"POINT(28.31449 60.92323)", +"http://www.yso.fi/onto/yso/p109981":"POINT(28.70852 62.04229)", +"http://www.yso.fi/onto/yso/p109982":"POINT(28.73929 62.06958)", +"http://www.yso.fi/onto/yso/p109983":"POINT(28.77823 61.99457)", +"http://www.yso.fi/onto/yso/p109987":"POINT(21.78822 60.57519)", +"http://www.yso.fi/onto/yso/p109988":"POINT(21.816667 60.616667)", +"http://www.yso.fi/onto/yso/p109991":"POINT(28.31449 60.92323)", +"http://www.yso.fi/onto/yso/p109993":"POINT(-6.38833 54.4472)", +"http://www.yso.fi/onto/yso/p109995":"POINT(26.32233 60.88889)", +"http://www.yso.fi/onto/yso/p109996":"POINT(27.03842 63.67293)", +"http://www.yso.fi/onto/yso/p109997":"POINT(29.872222222222 60.168888888889)", +"http://www.yso.fi/onto/yso/p109998":"POINT(25.20808 65.27653)", +"http://www.yso.fi/onto/yso/p109999":"POINT(25.06633 60.2055)", +"http://www.yso.fi/onto/yso/p110001":"", +"http://www.yso.fi/onto/yso/p110002":"POINT(25.63812 60.37309)", +"http://www.yso.fi/onto/yso/p110009":"POINT(28.7372 62.81645)", +"http://www.yso.fi/onto/yso/p110010":"POINT(23.49086 60.84202)", +"http://www.yso.fi/onto/yso/p110012":"POINT(27.4227 62.3793)", +"http://www.yso.fi/onto/yso/p110014":"POINT(28.00211 69.97136)", +"http://www.yso.fi/onto/yso/p110015":"POINT(25.0182 60.42505)", +"http://www.yso.fi/onto/yso/p110018":"POINT(25.08778 62.31559)", +"http://www.yso.fi/onto/yso/p110019":"POINT(25.7221 61.89)", +"http://www.yso.fi/onto/yso/p110023":"POINT(14.122222222222 40.823055555556)", +"http://www.yso.fi/onto/yso/p110024":"POINT(23.90012 61.70774)", +"http://www.yso.fi/onto/yso/p110025":"POINT(23.83333333 68.71666667)", +"http://www.yso.fi/onto/yso/p110027":"POINT(27.70224 61.27447)", +"http://www.yso.fi/onto/yso/p110028":"POINT(22.45619 60.55885)", +"http://www.yso.fi/onto/yso/p110029":"POINT(22.44921 60.55231)", +"http://www.yso.fi/onto/yso/p110030":"POINT(21.35384 62.12115)", +"http://www.yso.fi/onto/yso/p110033":"", +"http://www.yso.fi/onto/yso/p110033":"POINT(125 45)", +"http://www.yso.fi/onto/yso/p110034":"POINT(125 45)", +"http://www.yso.fi/onto/yso/p110048":"POINT(21.65878 63.10327)", +"http://www.yso.fi/onto/yso/p110054":"POINT(22.57699 61.45384)", +"http://www.yso.fi/onto/yso/p110071":"POINT(-5.347422 36.138706)", +"http://www.yso.fi/onto/yso/p110076":"POINT(29.96605 62.17088)", +"http://www.yso.fi/onto/yso/p110078":"POINT(23.50393 61.29658)", +"http://www.yso.fi/onto/yso/p110080":"POINT(6.7611111111111 51.432222222222)", +"http://www.yso.fi/onto/yso/p110082":"POINT(28.70035 61.18113)", +"http://www.yso.fi/onto/yso/p110085":"", +"http://www.yso.fi/onto/yso/p110088":"POINT(29.98895 62.16907)", +"http://www.yso.fi/onto/yso/p110089":"POINT(24.16777778 65.82583333)", +"http://www.yso.fi/onto/yso/p110097":"POINT(30.55 64.947222222222)", +"http://www.yso.fi/onto/yso/p110098":"POINT(41.901111111111 64.134166666667)", +"http://www.yso.fi/onto/yso/p110099":"", +"http://www.yso.fi/onto/yso/p110101":"POINT(27.68191 64.2387)", +"http://www.yso.fi/onto/yso/p110102":"POINT(10.718333333333 50.948888888889)", +"http://www.yso.fi/onto/yso/p110106":"POINT(23.26687 61.70601)", +"http://www.yso.fi/onto/yso/p110107":"POINT(23.4395 61.72802)", +"http://www.yso.fi/onto/yso/p110108":"POINT(21.91893 61.33565)", +"http://www.yso.fi/onto/yso/p110110":"POINT(21.80909 61.33474)", +"http://www.yso.fi/onto/yso/p110111":"POINT(21.97736 61.31042)", +"http://www.yso.fi/onto/yso/p110113":"POINT(26.1855 62.42364)", +"http://www.yso.fi/onto/yso/p110114":"POINT(26.96612 67.68433)", +"http://www.yso.fi/onto/yso/p110116":"POINT(55.5325 -21.114444444444)", +"http://www.yso.fi/onto/yso/p110117":"POINT(26.91042 61.02063)", +"http://www.yso.fi/onto/yso/p110118":"POINT(24.13936 61.37463)", +"http://www.yso.fi/onto/yso/p110119":"POINT(23.54603 60.2258)", +"http://www.yso.fi/onto/yso/p110125":"POINT(36.816666666667 -1.2833333333333)", +"http://www.yso.fi/onto/yso/p110128":"POINT(86.925277777778 27.988055555556)", +"http://www.yso.fi/onto/yso/p110129":"POINT(16.366666666667 56.666666666667)", +"http://www.yso.fi/onto/yso/p110130":"POINT(18.533333333333 54.5)", +"http://www.yso.fi/onto/yso/p110133":"POINT(27.46806 64.46994)", +"http://www.yso.fi/onto/yso/p110135":"POINT(20.37505 60.26436)", +"http://www.yso.fi/onto/yso/p110136":"POINT(28.2431 61.06364)", +"http://www.yso.fi/onto/yso/p110137":"POINT(24.97901 60.2086)", +"http://www.yso.fi/onto/yso/p110139":"POINT(27.00766 60.69355)", +"http://www.yso.fi/onto/yso/p110141":"POINT(-84.390277777778 33.756944444444)", +"http://www.yso.fi/onto/yso/p110146":"POINT(30.60765 62.19367)", +"http://www.yso.fi/onto/yso/p110147":"POINT(21.142777777778 55.7075)", +"http://www.yso.fi/onto/yso/p110152":"POINT(29.39397 62.61198)", +"http://www.yso.fi/onto/yso/p110157":"POINT(44 65)", +"http://www.yso.fi/onto/yso/p110159":"POINT(21.6918 60.85301)", +"http://www.yso.fi/onto/yso/p110160":"POINT(30.61758 62.19832)", +"http://www.yso.fi/onto/yso/p110161":"POINT(27.44381 62.3287)", +"http://www.yso.fi/onto/yso/p110162":"POINT(25.94336 61.10836)", +"http://www.yso.fi/onto/yso/p110163":"POINT(25.68428 60.37972)", +"http://www.yso.fi/onto/yso/p110164":"POINT(25.70837 62.28423)", +"http://www.yso.fi/onto/yso/p110166":"", +"http://www.yso.fi/onto/yso/p110167":"", +"http://www.yso.fi/onto/yso/p110168":"POINT(24.17484 60.08447)", +"http://www.yso.fi/onto/yso/p110169":"", +"http://www.yso.fi/onto/yso/p110170":"", +"http://www.yso.fi/onto/yso/p110171":"POINT(21.6119 61.5594)", +"http://www.yso.fi/onto/yso/p110172":"POINT(11.887 58.248)", +"http://www.yso.fi/onto/yso/p110173":"POINT(11.633333333333 58.016666666667)", +"http://www.yso.fi/onto/yso/p110174":"POINT(23.83933 61.48157)", +"http://www.yso.fi/onto/yso/p110175":"POINT(21.46845 60.68514)", +"http://www.yso.fi/onto/yso/p110176":"", +"http://www.yso.fi/onto/yso/p110180":"POINT(29.73686 62.59407)", +"http://www.yso.fi/onto/yso/p110183":"POINT(24.90553 60.22246)", +"http://www.yso.fi/onto/yso/p110184":"POINT(24.95229 60.23133)", +"http://www.yso.fi/onto/yso/p110185":"POINT(24.89335 60.21846)", +"http://www.yso.fi/onto/yso/p110186":"POINT(25.00515 60.18572)", +"http://www.yso.fi/onto/yso/p110191":"POINT(30.51666667 50.45)", +"http://www.yso.fi/onto/yso/p110195":"POINT(22.34974 60.18993)", +"http://www.yso.fi/onto/yso/p110196":"POINT(25.75394 62.09922)", +"http://www.yso.fi/onto/yso/p110197":"POINT(20.77569 60.34434)", +"http://www.yso.fi/onto/yso/p110198":"POINT(22.26353 61.99324)", +"http://www.yso.fi/onto/yso/p110199":"POINT(28.31799 61.06699)", +"http://www.yso.fi/onto/yso/p110200":"POINT(25.4297 65.02664)", +"http://www.yso.fi/onto/yso/p110204":"POINT(24.74719 60.28625)", +"http://www.yso.fi/onto/yso/p110237":"POINT(10.140555555556 54.325277777778)", +"http://www.yso.fi/onto/yso/p110240":"POINT(27.50452 62.77613)", +"http://www.yso.fi/onto/yso/p110241":"POINT(27.56787 62.76499)", +"http://www.yso.fi/onto/yso/p110242":"POINT(27.65558 62.7055)", +"http://www.yso.fi/onto/yso/p110243":"POINT(27.547 62.73512)", +"http://www.yso.fi/onto/yso/p110247":"POINT(27.31235 63.45446)", +"http://www.yso.fi/onto/yso/p110248":"POINT(27.14403 63.69715)", +"http://www.yso.fi/onto/yso/p110270":"POINT(27.39578 61.83302)", +"http://www.yso.fi/onto/yso/p110273":"POINT(25.6659 60.95937)", +"http://www.yso.fi/onto/yso/p110274":"POINT(25.67484 60.96645)", +"http://www.yso.fi/onto/yso/p110275":"POINT(27.71423 61.63457)", +"http://www.yso.fi/onto/yso/p110276":"POINT(23.76703 61.74578)", +"http://www.yso.fi/onto/yso/p110277":"POINT(27.43159 63.87317)", +"http://www.yso.fi/onto/yso/p110278":"POINT(26.90643 63.56219)", +"http://www.yso.fi/onto/yso/p110281":"POINT(125.75 -8.966667)", +"http://www.yso.fi/onto/yso/p110282":"", +"http://www.yso.fi/onto/yso/p110290":"POINT(28.3713 65.34411)", +"http://www.yso.fi/onto/yso/p110291":"POINT(27.91763 66.35022)", +"http://www.yso.fi/onto/yso/p110296":"POINT(22.21063 60.40175)", +"http://www.yso.fi/onto/yso/p110297":"", +"http://www.yso.fi/onto/yso/p110298":"POINT(29.670833333333 60.314722222222)", +"http://www.yso.fi/onto/yso/p110299":"", +"http://www.yso.fi/onto/yso/p110300":"POINT(25.05234 60.10186)", +"http://www.yso.fi/onto/yso/p110301":"POINT(25.97118 60.72754)", +"http://www.yso.fi/onto/yso/p110330":"POINT(28.43841 62.88383)", +"http://www.yso.fi/onto/yso/p110364":"POINT(22.52137 61.06502)", +"http://www.yso.fi/onto/yso/p110371":"POINT(25.49901 65.07422)", +"http://www.yso.fi/onto/yso/p110372":"POINT(23.94795 63.56537)", +"http://www.yso.fi/onto/yso/p110373":"POINT(25.86265 61.31242)", +"http://www.yso.fi/onto/yso/p110374":"POINT(21.94411 60.64459)", +"http://www.yso.fi/onto/yso/p110376":"POINT(22.89788333 62.48245)", +"http://www.yso.fi/onto/yso/p110377":"POINT(29.79688 62.58593)", +"http://www.yso.fi/onto/yso/p110378":"POINT(28.11682 62.05748)", +"http://www.yso.fi/onto/yso/p110379":"POINT(21.22539 62.38287)", +"http://www.yso.fi/onto/yso/p110380":"POINT(-85.916666666667 12.916666666667)", +"http://www.yso.fi/onto/yso/p110381":"POINT(25.13997 62.08144)", +"http://www.yso.fi/onto/yso/p110383":"POINT(22.16994 61.42624)", +"http://www.yso.fi/onto/yso/p110384":"POINT(22.16617 61.41363)", +"http://www.yso.fi/onto/yso/p110386":"POINT(21.69938 60.6126)", +"http://www.yso.fi/onto/yso/p110394":"POINT(21.011111111111 56.508333333333)", +"http://www.yso.fi/onto/yso/p110395":"POINT(28.83657 63.60751)", +"http://www.yso.fi/onto/yso/p110401":"", +"http://www.yso.fi/onto/yso/p110405":"POINT(21.59447 63.10908)", +"http://www.yso.fi/onto/yso/p110406":"POINT(23.97382 63.36433)", +"http://www.yso.fi/onto/yso/p110407":"POINT(21.84309 61.46504)", +"http://www.yso.fi/onto/yso/p110408":"POINT(27.72461 66.57859)", +"http://www.yso.fi/onto/yso/p110409":"POINT(27.70697 66.55812)", +"http://www.yso.fi/onto/yso/p110410":"POINT(23.54533 63.35249)", +"http://www.yso.fi/onto/yso/p110416":"POINT(-75 48)", +"http://www.yso.fi/onto/yso/p110418":"POINT(28.81457 63.61056)", +"http://www.yso.fi/onto/yso/p110420":"POINT(46.751944 39.815278)", +"http://www.yso.fi/onto/yso/p110426":"POINT(29.3351 62.96232)", +"http://www.yso.fi/onto/yso/p110429":"POINT(23.49684 59.98764)", +"http://www.yso.fi/onto/yso/p110430":"POINT(24.52781 65.76156)", +"http://www.yso.fi/onto/yso/p110431":"POINT(25.60766 66.46211)", +"http://www.yso.fi/onto/yso/p110432":"POINT(25.61238 66.47816)", +"http://www.yso.fi/onto/yso/p110433":"POINT(23.86708 60.23291)", +"http://www.yso.fi/onto/yso/p110434":"POINT(-4.5 54.25)", +"http://www.yso.fi/onto/yso/p110435":"POINT(-68.98944444 12.18611111)", +"http://www.yso.fi/onto/yso/p110436":"POINT(13.15 41.75)", +"http://www.yso.fi/onto/yso/p110437":"POINT(29.57602 62.59126)", +"http://www.yso.fi/onto/yso/p110438":"POINT(23.67697 61.51292)", +"http://www.yso.fi/onto/yso/p110439":"POINT(23.86708 60.23291)", +"http://www.yso.fi/onto/yso/p110447":"POINT(24.99833 62.74764)", +"http://www.yso.fi/onto/yso/p110448":"POINT(21.77399 61.82991)", +"http://www.yso.fi/onto/yso/p110449":"POINT(22.68063 62.51859)", +"http://www.yso.fi/onto/yso/p110502":"POINT(24.75143 64.00543)", +"http://www.yso.fi/onto/yso/p110503":"POINT(25.97894 60.31505)", +"http://www.yso.fi/onto/yso/p110504":"POINT(23.17822 63.69977)", +"http://www.yso.fi/onto/yso/p110505":"POINT(28.791111111111 60.5925)", +"http://www.yso.fi/onto/yso/p110506":"POINT(28.824334 60.604387)", +"http://www.yso.fi/onto/yso/p110510":"POINT(-135 64)", +"http://www.yso.fi/onto/yso/p110523":"POINT(24.64896 60.74279)", +"http://www.yso.fi/onto/yso/p110524":"POINT(24.57839 60.74854)", +"http://www.yso.fi/onto/yso/p110531":"POINT(29.15336 62.69838)", +"http://www.yso.fi/onto/yso/p110541":"POINT(22.31086 63.46774)", +"http://www.yso.fi/onto/yso/p110542":"POINT(30.20633 62.86725)", +"http://www.yso.fi/onto/yso/p110543":"POINT(24.89483 64.26399)", +"http://www.yso.fi/onto/yso/p110544":"POINT(24.91442 64.25185)", +"http://www.yso.fi/onto/yso/p110545":"POINT(23.62806 61.51086)", +"http://www.yso.fi/onto/yso/p110546":"POINT(25.70705 64.94829)", +"http://www.yso.fi/onto/yso/p110547":"POINT(25.3467 65.18978)", +"http://www.yso.fi/onto/yso/p110548":"POINT(25.61248 65.31229)", +"http://www.yso.fi/onto/yso/p110555":"POINT(28.58649 69.50503)", +"http://www.yso.fi/onto/yso/p110556":"POINT(28.63682 69.52839)", +"http://www.yso.fi/onto/yso/p110559":"POINT(29.48304 66.27066)", +"http://www.yso.fi/onto/yso/p110568":"POINT(35.655555555556 61.690555555556)", +"http://www.yso.fi/onto/yso/p110572":"POINT(25.39002 61.18938)", +"http://www.yso.fi/onto/yso/p110573":"POINT(25.42894 61.19398)", +"http://www.yso.fi/onto/yso/p110574":"POINT(23.32564 61.40193)", +"http://www.yso.fi/onto/yso/p110575":"POINT(-85.58 44.34)", +"http://www.yso.fi/onto/yso/p110619":"POINT(26.39911 62.35865)", +"http://www.yso.fi/onto/yso/p110620":"POINT(26.49 62.28338)", +"http://www.yso.fi/onto/yso/p110631":"POINT(23.656 -2.88)", +"http://www.yso.fi/onto/yso/p110632":"POINT(29.46445 63.62633)", +"http://www.yso.fi/onto/yso/p110633":"POINT(15.383330555556 -0.75)", +"http://www.yso.fi/onto/yso/p110636":"POINT(26.07064 63.25471)", +"http://www.yso.fi/onto/yso/p110637":"POINT(25.01983 60.30007)", +"http://www.yso.fi/onto/yso/p110638":"POINT(26.61601 60.93527)", +"http://www.yso.fi/onto/yso/p110640":"POINT(23.5325 61.60759)", +"http://www.yso.fi/onto/yso/p110641":"POINT(23.14848 60.82643)", +"http://www.yso.fi/onto/yso/p110643":"POINT(28.09914 66.98933)", +"http://www.yso.fi/onto/yso/p110645":"POINT(88.337777777778 22.541111111111)", +"http://www.yso.fi/onto/yso/p110646":"", +"http://www.yso.fi/onto/yso/p110651":"", +"http://www.yso.fi/onto/yso/p110654":"POINT(21.81018 61.46972)", +"http://www.yso.fi/onto/yso/p110667":"POINT(26.5897 65.72385)", +"http://www.yso.fi/onto/yso/p110668":"POINT(26.5897 65.72385)", +"http://www.yso.fi/onto/yso/p110671":"POINT(28.10762 64.0485)", +"http://www.yso.fi/onto/yso/p110672":"POINT(28.92033 62.96629)", +"http://www.yso.fi/onto/yso/p110673":"POINT(28.86812 62.95924)", +"http://www.yso.fi/onto/yso/p110674":"POINT(25.76741 66.51923)", +"http://www.yso.fi/onto/yso/p110675":"POINT(25.93904 62.2562)", +"http://www.yso.fi/onto/yso/p110676":"POINT(27.01983 69.75804)", +"http://www.yso.fi/onto/yso/p110684":"POINT(-9.5833333333333 30.416666666667)", +"http://www.yso.fi/onto/yso/p110692":"POINT(24.97801 61.51507)", +"http://www.yso.fi/onto/yso/p110694":"POINT(12.612222222222 23.079722222222)", +"http://www.yso.fi/onto/yso/p110710":"POINT(28.09911 61.76993)", +"http://www.yso.fi/onto/yso/p110711":"POINT(28.5803 62.57509)", +"http://www.yso.fi/onto/yso/p110712":"POINT(28.78037 61.76601)", +"http://www.yso.fi/onto/yso/p110714":"POINT(25.09695 60.26482)", +"http://www.yso.fi/onto/yso/p110721":"POINT(21.50118 61.41809)", +"http://www.yso.fi/onto/yso/p110723":"POINT(27.21905 62.01752)", +"http://www.yso.fi/onto/yso/p110738":"POINT(22.48591 68.45105)", +"http://www.yso.fi/onto/yso/p110744":"", +"http://www.yso.fi/onto/yso/p110749":"POINT(21.26119 62.61478)", +"http://www.yso.fi/onto/yso/p110750":"POINT(28.697222222222 60.729166666667)", +"http://www.yso.fi/onto/yso/p110752":"POINT(23.58481 61.36784)", +"http://www.yso.fi/onto/yso/p110753":"POINT(24.95166 60.18781)", +"http://www.yso.fi/onto/yso/p110754":"", +"http://www.yso.fi/onto/yso/p110755":"POINT(22.89589 60.29769)", +"http://www.yso.fi/onto/yso/p110756":"POINT(23.21462 61.44283)", +"http://www.yso.fi/onto/yso/p110757":"POINT(28.333333333333 57.816666666667)", +"http://www.yso.fi/onto/yso/p110761":"POINT(9.51416 54.470038)", +"http://www.yso.fi/onto/yso/p110762":"POINT(9.0833333333333 40.05)", +"http://www.yso.fi/onto/yso/p110773":"POINT(24.95229 60.23133)", +"http://www.yso.fi/onto/yso/p110774":"POINT(24.89335 60.21846)", +"http://www.yso.fi/onto/yso/p110775":"POINT(23.83933 61.48157)", +"http://www.yso.fi/onto/yso/p110784":"POINT(24.30501 60.07595)", +"http://www.yso.fi/onto/yso/p110824":"POINT(-89.125555555556 18.845)", +"http://www.yso.fi/onto/yso/p110830":"POINT(24.17484 60.08447)", +"http://www.yso.fi/onto/yso/p110845":"POINT(25.6554 62.39585)", +"http://www.yso.fi/onto/yso/p110846":"POINT(24.23561 61.55515)", +"http://www.yso.fi/onto/yso/p110847":"POINT(24.99152 62.99753)", +"http://www.yso.fi/onto/yso/p110851":"POINT(21.35885 63.34846)", +"http://www.yso.fi/onto/yso/p110852":"POINT(-89.5 44.5)", +"http://www.yso.fi/onto/yso/p110855":"POINT(28.67503 60.98656)", +"http://www.yso.fi/onto/yso/p110856":"POINT(14.489722 40.750556)", +"http://www.yso.fi/onto/yso/p110857":"POINT(28.67503 60.98656)", +"http://www.yso.fi/onto/yso/p110858":"POINT(28.67503 60.98656)", +"http://www.yso.fi/onto/yso/p110868":"POINT(24.78987 60.39606)", +"http://www.yso.fi/onto/yso/p110869":"POINT(26.7288 61.50158)", +"http://www.yso.fi/onto/yso/p110874":"POINT(28.68507 61.02163)", +"http://www.yso.fi/onto/yso/p110887":"POINT(25.94251 61.24344)", +"http://www.yso.fi/onto/yso/p110888":"POINT(27.59944 61.23719)", +"http://www.yso.fi/onto/yso/p110893":"POINT(28.22479 61.05945)", +"http://www.yso.fi/onto/yso/p110895":"POINT(25.8521 60.67044)", +"http://www.yso.fi/onto/yso/p110899":"POINT(24.9877 60.14781)", +"http://www.yso.fi/onto/yso/p110900":"POINT(31.275 58.525)", +"http://www.yso.fi/onto/yso/p110903":"POINT(24.98724 60.13963)", +"http://www.yso.fi/onto/yso/p110913":"POINT(22.27463 62.51138)", +"http://www.yso.fi/onto/yso/p110921":"POINT(23.65757 62.69567)", +"http://www.yso.fi/onto/yso/p110922":"POINT(16.2 58.6)", +"http://www.yso.fi/onto/yso/p110923":"POINT(10.383333333333 55.4)", +"http://www.yso.fi/onto/yso/p110925":"POINT(2.1863888888889 49.063611111111)", +"http://www.yso.fi/onto/yso/p110926":"POINT(12.916666666667 50.833333333333)", +"http://www.yso.fi/onto/yso/p110927":"POINT(-1.9749167 43.2661645)", +"http://www.yso.fi/onto/yso/p110928":"POINT(25.86088 62.32671)", +"http://www.yso.fi/onto/yso/p110931":"POINT(23.8947 66.9432)", +"http://www.yso.fi/onto/yso/p110935":"POINT(27.55232 64.97557)", +"http://www.yso.fi/onto/yso/p110938":"POINT(26.84797 61.78044)", +"http://www.yso.fi/onto/yso/p110939":"POINT(26.55367 63.26194)", +"http://www.yso.fi/onto/yso/p110940":"POINT(26.66283 63.19995)", +"http://www.yso.fi/onto/yso/p110941":"POINT(26.70694 63.14143)", +"http://www.yso.fi/onto/yso/p110942":"POINT(24.98722 60.17162)", +"http://www.yso.fi/onto/yso/p110943":"POINT(24.84376 60.27278)", +"http://www.yso.fi/onto/yso/p110944":"POINT(23.92582 60.29872)", +"http://www.yso.fi/onto/yso/p110945":"POINT(23.92582 60.29872)", +"http://www.yso.fi/onto/yso/p110946":"POINT(24.78504 60.77122)", +"http://www.yso.fi/onto/yso/p110948":"POINT(26.15085 63.95508)", +"http://www.yso.fi/onto/yso/p110955":"POINT(22 -23)", +"http://www.yso.fi/onto/yso/p110961":"POINT(21.97285 60.81764)", +"http://www.yso.fi/onto/yso/p110962":"POINT(29.6295 62.41992)", +"http://www.yso.fi/onto/yso/p110992":"POINT(25.82165 64.02549)", +"http://www.yso.fi/onto/yso/p110993":"POINT(24.11785 62.79685)", +"http://www.yso.fi/onto/yso/p110994":"POINT(24.9166 60.75741)", +"http://www.yso.fi/onto/yso/p110996":"POINT(29.48247 62.67804)", +"http://www.yso.fi/onto/yso/p110997":"POINT(29.56909 62.67716)", +"http://www.yso.fi/onto/yso/p110998":"POINT(24.78215 60.2571)", +"http://www.yso.fi/onto/yso/p111001":"POINT(24.62311 65.69227)", +"http://www.yso.fi/onto/yso/p111101":"POINT(29.82793 62.62418)", +"http://www.yso.fi/onto/yso/p111107":"POINT(25.51995 63.08968)", +"http://www.yso.fi/onto/yso/p111109":"POINT(24.50125 60.71159)", +"http://www.yso.fi/onto/yso/p111110":"POINT(26.78776 67.68357)", +"http://www.yso.fi/onto/yso/p111112":"POINT(22.77514 62.43208)", +"http://www.yso.fi/onto/yso/p111113":"POINT(22.98895 62.3705)", +"http://www.yso.fi/onto/yso/p111114":"POINT(23.90928 60.75256)", +"http://www.yso.fi/onto/yso/p111117":"POINT(-3.637848 52.349602)", +"http://www.yso.fi/onto/yso/p111125":"POINT(29.766666666667 60)", +"http://www.yso.fi/onto/yso/p111186":"POINT(25.18662 62.29674)", +"http://www.yso.fi/onto/yso/p111187":"POINT(21.85787 62.63935)", +"http://www.yso.fi/onto/yso/p111188":"POINT(29.15093 62.23497)", +"http://www.yso.fi/onto/yso/p111189":"POINT(26.75431 62.39915)", +"http://www.yso.fi/onto/yso/p111190":"", +"http://www.yso.fi/onto/yso/p111191":"POINT(31.033888888889 62.03)", +"http://www.yso.fi/onto/yso/p111192":"POINT(24.35467 61.05216)", +"http://www.yso.fi/onto/yso/p111193":"POINT(27.41407 60.53408)", +"http://www.yso.fi/onto/yso/p111195":"POINT(-8.1705555555556 76.668888888889)", +"http://www.yso.fi/onto/yso/p111196":"POINT(26.69381 61.81011)", +"http://www.yso.fi/onto/yso/p111201":"POINT(25.86217 60.66653)", +"http://www.yso.fi/onto/yso/p111202":"POINT(25.88047 60.68894)", +"http://www.yso.fi/onto/yso/p111204":"POINT(114 1)", +"http://www.yso.fi/onto/yso/p111214":"POINT(23.82538 68.26509)", +"http://www.yso.fi/onto/yso/p111216":"POINT(25.36285 61.7294)", +"http://www.yso.fi/onto/yso/p111217":"POINT(24.50584 62.13381)", +"http://www.yso.fi/onto/yso/p111221":"POINT(25.916666666667 -24.666666666667)", +"http://www.yso.fi/onto/yso/p111222":"", +"http://www.yso.fi/onto/yso/p111228":"POINT(24.28039 61.3368)", +"http://www.yso.fi/onto/yso/p111261":"POINT(24.46446 60.9959)", +"http://www.yso.fi/onto/yso/p111272":"", +"http://www.yso.fi/onto/yso/p111274":"", +"http://www.yso.fi/onto/yso/p111275":"POINT(22.6911 61.36983)", +"http://www.yso.fi/onto/yso/p111276":"POINT(22.72633 61.37124)", +"http://www.yso.fi/onto/yso/p111277":"POINT(22.75548 61.35408)", +"http://www.yso.fi/onto/yso/p111278":"POINT(23.31543 59.92628)", +"http://www.yso.fi/onto/yso/p111279":"POINT(23.12085 61.33076)", +"http://www.yso.fi/onto/yso/p111280":"POINT(23.18197 61.29098)", +"http://www.yso.fi/onto/yso/p111283":"POINT(26.02971 63.95983)", +"http://www.yso.fi/onto/yso/p111284":"POINT(26.05783 64.02231)", +"http://www.yso.fi/onto/yso/p111285":"POINT(27.69282 63.36542)", +"http://www.yso.fi/onto/yso/p111286":"POINT(27.6767 63.27653)", +"http://www.yso.fi/onto/yso/p111288":"POINT(26.26395 62.53351)", +"http://www.yso.fi/onto/yso/p111305":"POINT(24.4893 64.55194)", +"http://www.yso.fi/onto/yso/p111306":"POINT(24.38363 60.35294)", +"http://www.yso.fi/onto/yso/p111308":"POINT(23.96419 67.90729)", +"http://www.yso.fi/onto/yso/p111312":"POINT(25.0955 60.23946)", +"http://www.yso.fi/onto/yso/p111313":"POINT(-0.375 39.466666666667)", +"http://www.yso.fi/onto/yso/p111321":"POINT(29.37673 61.40856)", +"http://www.yso.fi/onto/yso/p111323":"POINT(24.9525 60.16944444)", +"http://www.yso.fi/onto/yso/p111333":"", +"http://www.yso.fi/onto/yso/p111334":"", +"http://www.yso.fi/onto/yso/p111335":"", +"http://www.yso.fi/onto/yso/p111335":"POINT(2.3411111111111 48.886944444444)", +"http://www.yso.fi/onto/yso/p111336":"POINT(23.67948 60.46088)", +"http://www.yso.fi/onto/yso/p111337":"POINT(25.43904 62.56391)", +"http://www.yso.fi/onto/yso/p111338":"", +"http://www.yso.fi/onto/yso/p111464":"POINT(23.34834 60.57816)", +"http://www.yso.fi/onto/yso/p111467":"POINT(24.68832 62.02565)", +"http://www.yso.fi/onto/yso/p111469":"POINT(28.63091 64.35401)", +"http://www.yso.fi/onto/yso/p111472":"POINT(25.95593 63.77737)", +"http://www.yso.fi/onto/yso/p111473":"POINT(25.86912 63.82951)", +"http://www.yso.fi/onto/yso/p111481":"POINT(60 45)", +"http://www.yso.fi/onto/yso/p111482":"POINT(51 42)", +"http://www.yso.fi/onto/yso/p111483":"", +"http://www.yso.fi/onto/yso/p111485":"POINT(31.1 30.166667)", +"http://www.yso.fi/onto/yso/p111505":"", +"http://www.yso.fi/onto/yso/p111524":"POINT(26.9574 65.54369)", +"http://www.yso.fi/onto/yso/p111525":"POINT(23.2069 60.18299)", +"http://www.yso.fi/onto/yso/p111529":"", +"http://www.yso.fi/onto/yso/p111540":"POINT(80.219421 6.039505)", +"http://www.yso.fi/onto/yso/p111541":"POINT(163 72)", +"http://www.yso.fi/onto/yso/p111548":"POINT(78 20)", +"http://www.yso.fi/onto/yso/p111554":"POINT(-81 34)", +"http://www.yso.fi/onto/yso/p111562":"POINT(25.82187 60.87158)", +"http://www.yso.fi/onto/yso/p111563":"POINT(25.03551 60.17989)", +"http://www.yso.fi/onto/yso/p111564":"POINT(24.94391 60.17411)", +"http://www.yso.fi/onto/yso/p111565":"POINT(24.05343 61.20408)", +"http://www.yso.fi/onto/yso/p111567":"POINT(21.24257 62.47101)", +"http://www.yso.fi/onto/yso/p111568":"POINT(25.93363 65.35597)", +"http://www.yso.fi/onto/yso/p111569":"", +"http://www.yso.fi/onto/yso/p111611":"POINT(24.7478 60.25249)", +"http://www.yso.fi/onto/yso/p111662":"POINT(27.70322 64.45203)", +"http://www.yso.fi/onto/yso/p111663":"POINT(29.99408 63.32248)", +"http://www.yso.fi/onto/yso/p111664":"POINT(23.46613 61.85059)", +"http://www.yso.fi/onto/yso/p111665":"POINT(23.34046 61.83713)", +"http://www.yso.fi/onto/yso/p111666":"POINT(23.34105 61.80802)", +"http://www.yso.fi/onto/yso/p111668":"POINT(-92.408611111111 16.41)", +"http://www.yso.fi/onto/yso/p111676":"", +"http://www.yso.fi/onto/yso/p111686":"POINT(25.66061 61.01323)", +"http://www.yso.fi/onto/yso/p111687":"POINT(21.84184 61.22144)", +"http://www.yso.fi/onto/yso/p111689":"POINT(21.97504 61.22348)", +"http://www.yso.fi/onto/yso/p111691":"POINT(27.12412 64.382)", +"http://www.yso.fi/onto/yso/p111692":"POINT(23.77756 61.88697)", +"http://www.yso.fi/onto/yso/p111693":"POINT(23.80576 61.48921)", +"http://www.yso.fi/onto/yso/p111694":"POINT(23.79899 61.48028)", +"http://www.yso.fi/onto/yso/p111695":"POINT(23.77981 61.48627)", +"http://www.yso.fi/onto/yso/p111696":"POINT(23.799 61.50808)", +"http://www.yso.fi/onto/yso/p111697":"POINT(22.01674 61.45996)", +"http://www.yso.fi/onto/yso/p111698":"POINT(21.633333333333 67.666666666667)", +"http://www.yso.fi/onto/yso/p111699":"", +"http://www.yso.fi/onto/yso/p111706":"POINT(14.759722222222 40.680555555556)", +"http://www.yso.fi/onto/yso/p111707":"POINT(-1 41)", +"http://www.yso.fi/onto/yso/p111715":"POINT(-110 47)", +"http://www.yso.fi/onto/yso/p111716":"POINT(-105.5 39)", +"http://www.yso.fi/onto/yso/p111717":"POINT(-111.5 39.5)", +"http://www.yso.fi/onto/yso/p111718":"POINT(-106.44527778 39.11694444)", +"http://www.yso.fi/onto/yso/p111725":"POINT(22.24794 60.45022)", +"http://www.yso.fi/onto/yso/p111726":"POINT(24.46444 60.99599)", +"http://www.yso.fi/onto/yso/p111731":"POINT(24.23611 60.12516)", +"http://www.yso.fi/onto/yso/p111733":"POINT(27.59461 65.24155)", +"http://www.yso.fi/onto/yso/p111734":"POINT(27.64237 65.16345)", +"http://www.yso.fi/onto/yso/p111735":"POINT(27.56664 65.1122)", +"http://www.yso.fi/onto/yso/p111737":"POINT(27.83364 65.19262)", +"http://www.yso.fi/onto/yso/p111739":"POINT(27.55169 68.65231)", +"http://www.yso.fi/onto/yso/p111743":"POINT(2.9 46.5)", +"http://www.yso.fi/onto/yso/p111744":"POINT(11.1 50.9)", +"http://www.yso.fi/onto/yso/p111745":"POINT(27.227 61.7011)", +"http://www.yso.fi/onto/yso/p111747":"POINT(24.20987 60.30093)", +"http://www.yso.fi/onto/yso/p111756":"POINT(25.1383 60.19237)", +"http://www.yso.fi/onto/yso/p111757":"POINT(25.11858 60.19972)", +"http://www.yso.fi/onto/yso/p111762":"POINT(9.6775 50.552777777778)", +"http://www.yso.fi/onto/yso/p111763":"POINT(10.429163 51.906003)", +"http://www.yso.fi/onto/yso/p111764":"POINT(24.94691 60.16501)", +"http://www.yso.fi/onto/yso/p111765":"POINT(24.94582 60.17475)", +"http://www.yso.fi/onto/yso/p111766":"POINT(28.00506 62.99563)", +"http://www.yso.fi/onto/yso/p111768":"", +"http://www.yso.fi/onto/yso/p111769":"POINT(32.2558 18.9508)", +"http://www.yso.fi/onto/yso/p111773":"POINT(28.801111111111 60.874166666667)", +"http://www.yso.fi/onto/yso/p111774":"POINT(27.74737 61.21267)", +"http://www.yso.fi/onto/yso/p111775":"POINT(3.22 51.209444444444)", +"http://www.yso.fi/onto/yso/p111776":"POINT(-9.1666666666667 38.716666666667)", +"http://www.yso.fi/onto/yso/p111779":"POINT(138.19277777778 36.648611111111)", +"http://www.yso.fi/onto/yso/p111781":"POINT(22.99426 61.29037)", +"http://www.yso.fi/onto/yso/p111782":"POINT(22.69504 61.37578)", +"http://www.yso.fi/onto/yso/p111783":"POINT(22.91909 61.25055)", +"http://www.yso.fi/onto/yso/p111825":"", +"http://www.yso.fi/onto/yso/p111826":"POINT(27.64028 68.73283)", +"http://www.yso.fi/onto/yso/p111827":"POINT(25.86973 65.08156)", +"http://www.yso.fi/onto/yso/p111828":"POINT(28.43849 62.57946)", +"http://www.yso.fi/onto/yso/p111829":"POINT(28.29158 62.55508)", +"http://www.yso.fi/onto/yso/p111830":"POINT(23.85523 62.38671)", +"http://www.yso.fi/onto/yso/p111831":"POINT(27.58297 62.35645)", +"http://www.yso.fi/onto/yso/p111832":"POINT(27.5249 62.37393)", +"http://www.yso.fi/onto/yso/p111833":"", +"http://www.yso.fi/onto/yso/p111834":"POINT(28.17253 63.30411)", +"http://www.yso.fi/onto/yso/p111835":"POINT(28.06475 63.32557)", +"http://www.yso.fi/onto/yso/p111836":"POINT(28.16944 63.34927)", +"http://www.yso.fi/onto/yso/p111837":"POINT(29.5559 61.77877)", +"http://www.yso.fi/onto/yso/p111838":"", +"http://www.yso.fi/onto/yso/p111839":"POINT(29.29638 61.34566)", +"http://www.yso.fi/onto/yso/p111840":"POINT(29.29846 61.33733)", +"http://www.yso.fi/onto/yso/p111844":"POINT(22.63883 63.41354)", +"http://www.yso.fi/onto/yso/p111845":"", +"http://www.yso.fi/onto/yso/p111846":"POINT(21.46849 62.30198)", +"http://www.yso.fi/onto/yso/p111847":"POINT(21.455556 62.301389)", +"http://www.yso.fi/onto/yso/p111882":"POINT(24.85812 60.21357)", +"http://www.yso.fi/onto/yso/p111885":"POINT(25.83089 69.38481)", +"http://www.yso.fi/onto/yso/p111886":"POINT(23.78031 63.39406)", +"http://www.yso.fi/onto/yso/p111890":"POINT(25.45806 63.3879)", +"http://www.yso.fi/onto/yso/p111896":"POINT(23.75729 61.26593)", +"http://www.yso.fi/onto/yso/p111897":"POINT(25.41601 64.10925)", +"http://www.yso.fi/onto/yso/p111905":"POINT(21.1847 62.97276)", +"http://www.yso.fi/onto/yso/p111908":"POINT(24.97449 66.14708)", +"http://www.yso.fi/onto/yso/p111910":"POINT(24.29016 64.23578)", +"http://www.yso.fi/onto/yso/p111914":"POINT(25.10829 60.21166)", +"http://www.yso.fi/onto/yso/p111915":"", +"http://www.yso.fi/onto/yso/p111916":"POINT(27.72505 62.89833)", +"http://www.yso.fi/onto/yso/p111917":"POINT(24.4541 62.25389)", +"http://www.yso.fi/onto/yso/p111918":"POINT(22.07001 62.90112)", +"http://www.yso.fi/onto/yso/p111919":"POINT(22.12914 62.85296)", +"http://www.yso.fi/onto/yso/p111920":"POINT(31.07863 63.03382)", +"http://www.yso.fi/onto/yso/p111921":"POINT(30.75145 62.70419)", +"http://www.yso.fi/onto/yso/p111923":"POINT(23.34518 60.12879)", +"http://www.yso.fi/onto/yso/p111924":"POINT(25.17452 60.25489)", +"http://www.yso.fi/onto/yso/p111934":"POINT(20.66742 60.12999)", +"http://www.yso.fi/onto/yso/p111938":"POINT(19.838 60.19063)", +"http://www.yso.fi/onto/yso/p111942":"POINT(22.70031 61.18337)", +"http://www.yso.fi/onto/yso/p111943":"POINT(23.63972 60.08237)", +"http://www.yso.fi/onto/yso/p111944":"POINT(23.25892 61.68002)", +"http://www.yso.fi/onto/yso/p111945":"POINT(23.52477 68.02456)", +"http://www.yso.fi/onto/yso/p111947":"POINT(28.0602 64.2394)", +"http://www.yso.fi/onto/yso/p111948":"POINT(22.29334 60.44489)", +"http://www.yso.fi/onto/yso/p111980":"POINT(-77.018611111111 -12.035)", +"http://www.yso.fi/onto/yso/p112007":"POINT(26.541666666667 37.325)", +"http://www.yso.fi/onto/yso/p112021":"POINT(25.3542 65.28773)", +"http://www.yso.fi/onto/yso/p112023":"POINT(25.37617 62.64449)", +"http://www.yso.fi/onto/yso/p112024":"POINT(24.47445 61.89005)", +"http://www.yso.fi/onto/yso/p112029":"POINT(25.41919 66.4196)", +"http://www.yso.fi/onto/yso/p112031":"POINT(24.09933 61.54908)", +"http://www.yso.fi/onto/yso/p112032":"POINT(21.35384 62.12115)", +"http://www.yso.fi/onto/yso/p112034":"POINT(22.8884 62.31698)", +"http://www.yso.fi/onto/yso/p112035":"POINT(22.82988 62.31143)", +"http://www.yso.fi/onto/yso/p112036":"POINT(22.94595 62.31544)", +"http://www.yso.fi/onto/yso/p112037":"POINT(24.93597 60.22242)", +"http://www.yso.fi/onto/yso/p112038":"POINT(21.301 62.48888)", +"http://www.yso.fi/onto/yso/p112039":"POINT(25.34951 65.13523)", +"http://www.yso.fi/onto/yso/p112045":"POINT(29.20619 64.70917)", +"http://www.yso.fi/onto/yso/p112064":"POINT(24.46065 60.62608)", +"http://www.yso.fi/onto/yso/p112065":"POINT(24.28261 60.62557)", +"http://www.yso.fi/onto/yso/p112066":"POINT(24.29565 60.76723)", +"http://www.yso.fi/onto/yso/p112067":"POINT(24.13993 60.78234)", +"http://www.yso.fi/onto/yso/p112087":"", +"http://www.yso.fi/onto/yso/p112092":"", +"http://www.yso.fi/onto/yso/p112094":"", +"http://www.yso.fi/onto/yso/p112100":"POINT(21.58917 61.27488)", +"http://www.yso.fi/onto/yso/p112102":"POINT(25.69538 62.24039)", +"http://www.yso.fi/onto/yso/p112103":"POINT(25.68071 62.23872)", +"http://www.yso.fi/onto/yso/p112105":"POINT(24.46877 61.00374)", +"http://www.yso.fi/onto/yso/p112108":"POINT(27.82859 61.8314)", +"http://www.yso.fi/onto/yso/p112118":"POINT(25.43816 60.8373)", +"http://www.yso.fi/onto/yso/p112119":"POINT(26.00092 60.87029)", +"http://www.yso.fi/onto/yso/p112120":"POINT(25.80286 60.87675)", +"http://www.yso.fi/onto/yso/p112121":"POINT(25.48486 60.86156)", +"http://www.yso.fi/onto/yso/p112122":"POINT(25.60738 60.73251)", +"http://www.yso.fi/onto/yso/p112123":"POINT(25.67978 60.81028)", +"http://www.yso.fi/onto/yso/p112124":"POINT(25.92225 60.78491)", +"http://www.yso.fi/onto/yso/p112125":"POINT(25.7782 60.7363)", +"http://www.yso.fi/onto/yso/p112126":"POINT(25.69339 60.89309)", +"http://www.yso.fi/onto/yso/p112128":"POINT(25.67391 60.74592)", +"http://www.yso.fi/onto/yso/p112129":"POINT(25.45172 60.85936)", +"http://www.yso.fi/onto/yso/p112130":"POINT(25.70379 60.79762)", +"http://www.yso.fi/onto/yso/p112131":"POINT(26.02666 60.86876)", +"http://www.yso.fi/onto/yso/p112132":"POINT(26.06937 60.80118)", +"http://www.yso.fi/onto/yso/p112133":"POINT(25.5916 60.84738)", +"http://www.yso.fi/onto/yso/p112134":"POINT(25.7223 60.85608)", +"http://www.yso.fi/onto/yso/p112140":"", +"http://www.yso.fi/onto/yso/p112142":"POINT(22.64247 68.43553)", +"http://www.yso.fi/onto/yso/p112146":"POINT(24.75141 60.69901)", +"http://www.yso.fi/onto/yso/p112148":"", +"http://www.yso.fi/onto/yso/p112151":"POINT(22.48375 60.97014)", +"http://www.yso.fi/onto/yso/p112152":"POINT(22.85269 63.58242)", +"http://www.yso.fi/onto/yso/p112153":"POINT(25.06741 60.22207)", +"http://www.yso.fi/onto/yso/p112156":"POINT(24.21575 60.16043)", +"http://www.yso.fi/onto/yso/p112157":"POINT(24.27319 60.11828)", +"http://www.yso.fi/onto/yso/p112160":"POINT(25.57938 60.34792)", +"http://www.yso.fi/onto/yso/p112163":"POINT(22.44119 59.97734)", +"http://www.yso.fi/onto/yso/p112164":"POINT(22.76184 60.69921)", +"http://www.yso.fi/onto/yso/p112168":"", +"http://www.yso.fi/onto/yso/p112172":"POINT(25.00133 60.23102)", +"http://www.yso.fi/onto/yso/p112173":"POINT(29.40329 61.70873)", +"http://www.yso.fi/onto/yso/p112179":"POINT(17.233333333333 60.383333333333)", +"http://www.yso.fi/onto/yso/p112184":"POINT(-107.816317 37.939153)", +"http://www.yso.fi/onto/yso/p112185":"POINT(24.90112 60.16619)", +"http://www.yso.fi/onto/yso/p112190":"POINT(22.33071 60.21669)", +"http://www.yso.fi/onto/yso/p112191":"POINT(32.1754 61.2708)", +"http://www.yso.fi/onto/yso/p112194":"POINT(26.30955 62.27717)", +"http://www.yso.fi/onto/yso/p112196":"POINT(105.81375 15.105528)", +"http://www.yso.fi/onto/yso/p112198":"POINT(22.9953 61.86867)", +"http://www.yso.fi/onto/yso/p112200":"POINT(22.99906 61.89369)", +"http://www.yso.fi/onto/yso/p112201":"POINT(23.24499 59.90315)", +"http://www.yso.fi/onto/yso/p112203":"POINT(24.1013 61.51251)", +"http://www.yso.fi/onto/yso/p112204":"", +"http://www.yso.fi/onto/yso/p112207":"POINT(26.06429 66.46242)", +"http://www.yso.fi/onto/yso/p112208":"POINT(25.58502 61.04175)", +"http://www.yso.fi/onto/yso/p112209":"POINT(27.28082 61.60066)", +"http://www.yso.fi/onto/yso/p112222":"POINT(25.76378 61.04123)", +"http://www.yso.fi/onto/yso/p112223":"POINT(25.65582 61.06046)", +"http://www.yso.fi/onto/yso/p112224":"POINT(25.65314 61.04322)", +"http://www.yso.fi/onto/yso/p112225":"POINT(25.46587 61.07154)", +"http://www.yso.fi/onto/yso/p112226":"POINT(25.59778 61.09017)", +"http://www.yso.fi/onto/yso/p112227":"POINT(25.56272 60.96651)", +"http://www.yso.fi/onto/yso/p112228":"POINT(25.66338 61.09714)", +"http://www.yso.fi/onto/yso/p112229":"POINT(25.48435 61.04815)", +"http://www.yso.fi/onto/yso/p112230":"POINT(25.54104 61.10087)", +"http://www.yso.fi/onto/yso/p112241":"POINT(15.633055555556 56.183055555556)", +"http://www.yso.fi/onto/yso/p112243":"POINT(27.78523 64.74439)", +"http://www.yso.fi/onto/yso/p112246":"POINT(26.76666667 66.71666667)", +"http://www.yso.fi/onto/yso/p112248":"POINT(25.73852 62.53169)", +"http://www.yso.fi/onto/yso/p112249":"POINT(29.35965 65.40083)", +"http://www.yso.fi/onto/yso/p112251":"POINT(27.89945 62.32633)", +"http://www.yso.fi/onto/yso/p112253":"POINT(22.06772 62.93724)", +"http://www.yso.fi/onto/yso/p112254":"POINT(22.15756 62.94157)", +"http://www.yso.fi/onto/yso/p112255":"POINT(29.88464 62.97047)", +"http://www.yso.fi/onto/yso/p112258":"POINT(24.15329 62.56234)", +"http://www.yso.fi/onto/yso/p112259":"POINT(24.16636 62.52532)", +"http://www.yso.fi/onto/yso/p112334":"POINT(24.8637 60.2017)", +"http://www.yso.fi/onto/yso/p112335":"POINT(25.08885 61.29901)", +"http://www.yso.fi/onto/yso/p112336":"POINT(24.97253 61.28586)", +"http://www.yso.fi/onto/yso/p112337":"POINT(24.91505 61.36323)", +"http://www.yso.fi/onto/yso/p112338":"POINT(25.03694 61.35285)", +"http://www.yso.fi/onto/yso/p112340":"POINT(23.53397 62.71032)", +"http://www.yso.fi/onto/yso/p112341":"POINT(24.70972 60.11685)", +"http://www.yso.fi/onto/yso/p112344":"POINT(21.61775 61.43905)", +"http://www.yso.fi/onto/yso/p112345":"POINT(27.20313 62.06685)", +"http://www.yso.fi/onto/yso/p112346":"POINT(24.00321 60.20096)", +"http://www.yso.fi/onto/yso/p112347":"POINT(24.00321 60.20096)", +"http://www.yso.fi/onto/yso/p112348":"POINT(22.416389 37.45)", +"http://www.yso.fi/onto/yso/p112355":"POINT(18.923 50.347)", +"http://www.yso.fi/onto/yso/p112356":"POINT(29.51304 65.32119)", +"http://www.yso.fi/onto/yso/p112357":"POINT(24.67205 62.17921)", +"http://www.yso.fi/onto/yso/p112358":"POINT(27.54507 60.43487)", +"http://www.yso.fi/onto/yso/p112381":"", +"http://www.yso.fi/onto/yso/p112392":"POINT(25.42245 65.06741)", +"http://www.yso.fi/onto/yso/p112421":"POINT(21.41484 62.85809)", +"http://www.yso.fi/onto/yso/p112449":"POINT(27.36249 62.0057)", +"http://www.yso.fi/onto/yso/p112456":"POINT(24.29999 61.02763)", +"http://www.yso.fi/onto/yso/p112462":"POINT(25.42861 65.04029)", +"http://www.yso.fi/onto/yso/p112464":"POINT(24.18778 67.39597)", +"http://www.yso.fi/onto/yso/p112465":"POINT(23.56795 61.65988)", +"http://www.yso.fi/onto/yso/p112466":"POINT(22.63465 63.59092)", +"http://www.yso.fi/onto/yso/p112467":"POINT(22.26094 63.14758)", +"http://www.yso.fi/onto/yso/p112468":"POINT(30.810555555556 63.833611111111)", +"http://www.yso.fi/onto/yso/p112469":"POINT(29.355555555556 60.525)", +"http://www.yso.fi/onto/yso/p112470":"POINT(22.36692 62.9971)", +"http://www.yso.fi/onto/yso/p112472":"", +"http://www.yso.fi/onto/yso/p112476":"POINT(28.35055 61.66331)", +"http://www.yso.fi/onto/yso/p112486":"POINT(24.21879 67.28146)", +"http://www.yso.fi/onto/yso/p112488":"POINT(25.3598 63.44279)", +"http://www.yso.fi/onto/yso/p112489":"POINT(23.90456 63.05711)", +"http://www.yso.fi/onto/yso/p112490":"POINT(30.95225 62.76023)", +"http://www.yso.fi/onto/yso/p112491":"POINT(24.93308 64.29949)", +"http://www.yso.fi/onto/yso/p112492":"POINT(28.87057 63.69485)", +"http://www.yso.fi/onto/yso/p112502":"POINT(24.98418 60.17483)", +"http://www.yso.fi/onto/yso/p112505":"POINT(22.90016 63.83135)", +"http://www.yso.fi/onto/yso/p112506":"POINT(25.81722 65.09943)", +"http://www.yso.fi/onto/yso/p112513":"POINT(25.39725 65.20012)", +"http://www.yso.fi/onto/yso/p112514":"POINT(115.13833333333 -8.3691666666667)", +"http://www.yso.fi/onto/yso/p112515":"POINT(23.04234 61.82579)", +"http://www.yso.fi/onto/yso/p112516":"POINT(22.5443 60.90755)", +"http://www.yso.fi/onto/yso/p112517":"POINT(22.50001 60.91977)", +"http://www.yso.fi/onto/yso/p112523":"POINT(24.9516 60.16211)", +"http://www.yso.fi/onto/yso/p112525":"POINT(27.45114 68.08623)", +"http://www.yso.fi/onto/yso/p112612":"POINT(25.10755 60.38615)", +"http://www.yso.fi/onto/yso/p112613":"POINT(23.72254 61.45846)", +"http://www.yso.fi/onto/yso/p112614":"POINT(24.61351 60.21213)", +"http://www.yso.fi/onto/yso/p112615":"POINT(25.34905 61.14934)", +"http://www.yso.fi/onto/yso/p112616":"POINT(25.32737 61.14079)", +"http://www.yso.fi/onto/yso/p112618":"POINT(25.29057 60.93888)", +"http://www.yso.fi/onto/yso/p112619":"POINT(21.71913 63.04073)", +"http://www.yso.fi/onto/yso/p112621":"POINT(27.19332 62.4894)", +"http://www.yso.fi/onto/yso/p112622":"POINT(23.65172 63.57981)", +"http://www.yso.fi/onto/yso/p112623":"POINT(23.64034 63.58372)", +"http://www.yso.fi/onto/yso/p112632":"POINT(27.49637 61.49889)", +"http://www.yso.fi/onto/yso/p112633":"POINT(27.49508 61.55851)", +"http://www.yso.fi/onto/yso/p112634":"POINT(27.99418 61.47806)", +"http://www.yso.fi/onto/yso/p112635":"POINT(27.43508 61.44566)", +"http://www.yso.fi/onto/yso/p112652":"POINT(26.49476 61.73838)", +"http://www.yso.fi/onto/yso/p112653":"POINT(24.68752 60.50344)", +"http://www.yso.fi/onto/yso/p112656":"POINT(24.80443 61.30178)", +"http://www.yso.fi/onto/yso/p112657":"POINT(24.79954 61.30259)", +"http://www.yso.fi/onto/yso/p112658":"POINT(28.48443 62.16458)", +"http://www.yso.fi/onto/yso/p112659":"POINT(28.60951 62.11168)", +"http://www.yso.fi/onto/yso/p112661":"POINT(25.11553 60.41943)", +"http://www.yso.fi/onto/yso/p112662":"POINT(24.04901 61.65356)", +"http://www.yso.fi/onto/yso/p112663":"POINT(26.01329 61.00136)", +"http://www.yso.fi/onto/yso/p112664":"POINT(26.04582 60.98732)", +"http://www.yso.fi/onto/yso/p112665":"POINT(26.06353 60.97627)", +"http://www.yso.fi/onto/yso/p112666":"POINT(23.94322 60.38344)", +"http://www.yso.fi/onto/yso/p112667":"POINT(11.35 46.5)", +"http://www.yso.fi/onto/yso/p112668":"POINT(7.15 46.8)", +"http://www.yso.fi/onto/yso/p112671":"POINT(24.82744 60.33359)", +"http://www.yso.fi/onto/yso/p112673":"POINT(14.966666666667 58.033333333333)", +"http://www.yso.fi/onto/yso/p112674":"POINT(24.80543 62.3013)", +"http://www.yso.fi/onto/yso/p112675":"POINT(24.81094 62.31784)", +"http://www.yso.fi/onto/yso/p112676":"POINT(27.57901 60.55034)", +"http://www.yso.fi/onto/yso/p112677":"POINT(26.84936 60.86304)", +"http://www.yso.fi/onto/yso/p112678":"POINT(26.83869 60.90251)", +"http://www.yso.fi/onto/yso/p112681":"POINT(22.99247 61.33394)", +"http://www.yso.fi/onto/yso/p112685":"POINT(10.993741666667 45.438158333333)", +"http://www.yso.fi/onto/yso/p112686":"POINT(26.40141 61.42506)", +"http://www.yso.fi/onto/yso/p112689":"POINT(23.88398 60.01731)", +"http://www.yso.fi/onto/yso/p112699":"POINT(22.39044 60.60144)", +"http://www.yso.fi/onto/yso/p112702":"POINT(26.9908 63.04219)", +"http://www.yso.fi/onto/yso/p112705":"POINT(24.41651 60.28054)", +"http://www.yso.fi/onto/yso/p112706":"POINT(23.74771 61.45351)", +"http://www.yso.fi/onto/yso/p112707":"POINT(23.75143 61.43829)", +"http://www.yso.fi/onto/yso/p112708":"POINT(23.76244 61.46015)", +"http://www.yso.fi/onto/yso/p112709":"POINT(25.75517 62.26388)", +"http://www.yso.fi/onto/yso/p112710":"POINT(32.798611111111 60.509166666667)", +"http://www.yso.fi/onto/yso/p112711":"POINT(26.23305 64.68493)", +"http://www.yso.fi/onto/yso/p112712":"POINT(20.833333333333 42.55)", +"http://www.yso.fi/onto/yso/p112715":"POINT(28.5777 69.76495)", +"http://www.yso.fi/onto/yso/p112738":"POINT(100.51666666667 13.75)", +"http://www.yso.fi/onto/yso/p112739":"POINT(24.42047 64.66812)", +"http://www.yso.fi/onto/yso/p112741":"POINT(29.998611111111 61.496666666667)", +"http://www.yso.fi/onto/yso/p112746":"POINT(27.75434 62.23537)", +"http://www.yso.fi/onto/yso/p112747":"POINT(24.91694 62.3374)", +"http://www.yso.fi/onto/yso/p112748":"POINT(25.02775 61.71164)", +"http://www.yso.fi/onto/yso/p112751":"POINT(-66.5 18.25)", +"http://www.yso.fi/onto/yso/p112755":"POINT(31.1168 62.68402)", +"http://www.yso.fi/onto/yso/p112785":"POINT(23.284 69.900944444444)", +"http://www.yso.fi/onto/yso/p112786":"POINT(22.002 62.16373)", +"http://www.yso.fi/onto/yso/p112787":"POINT(24.70004 62.40328)", +"http://www.yso.fi/onto/yso/p112792":"POINT(24.67367 62.02846)", +"http://www.yso.fi/onto/yso/p112793":"POINT(24.10531 63.46274)", +"http://www.yso.fi/onto/yso/p112796":"POINT(85.366666666667 27.716666666667)", +"http://www.yso.fi/onto/yso/p112857":"POINT(23.42037 60.50404)", +"http://www.yso.fi/onto/yso/p112858":"POINT(23.39274 60.58112)", +"http://www.yso.fi/onto/yso/p112860":"", +"http://www.yso.fi/onto/yso/p112869":"POINT(26.16974 62.2018)", +"http://www.yso.fi/onto/yso/p112870":"POINT(24.27781 60.4474)", +"http://www.yso.fi/onto/yso/p112875":"", +"http://www.yso.fi/onto/yso/p112876":"POINT(24.647777777778 58.403055555556)", +"http://www.yso.fi/onto/yso/p112877":"POINT(-80 40.45)", +"http://www.yso.fi/onto/yso/p112878":"POINT(25.06035 60.49498)", +"http://www.yso.fi/onto/yso/p112879":"POINT(27.30965 62.41321)", +"http://www.yso.fi/onto/yso/p112880":"POINT(27.2738 62.39142)", +"http://www.yso.fi/onto/yso/p112881":"POINT(21.23986 62.66809)", +"http://www.yso.fi/onto/yso/p112882":"POINT(23.44516 60.48796)", +"http://www.yso.fi/onto/yso/p112883":"POINT(23.97532 64.34241)", +"http://www.yso.fi/onto/yso/p112885":"POINT(26.58405 65.925)", +"http://www.yso.fi/onto/yso/p112886":"POINT(24.15684 60.99485)", +"http://www.yso.fi/onto/yso/p112887":"", +"http://www.yso.fi/onto/yso/p112889":"POINT(25.76188 66.63275)", +"http://www.yso.fi/onto/yso/p112906":"POINT(24.19271 60.98258)", +"http://www.yso.fi/onto/yso/p112907":"POINT(24.61944 65.73088)", +"http://www.yso.fi/onto/yso/p112908":"POINT(25.7954 66.62392)", +"http://www.yso.fi/onto/yso/p112910":"POINT(29.93682 64.21988)", +"http://www.yso.fi/onto/yso/p112922":"POINT(22.01433 61.54756)", +"http://www.yso.fi/onto/yso/p112923":"POINT(22.08966 61.51473)", +"http://www.yso.fi/onto/yso/p112925":"POINT(22.02618 63.07523)", +"http://www.yso.fi/onto/yso/p112926":"POINT(21.62824 60.91778)", +"http://www.yso.fi/onto/yso/p112931":"POINT(21.62824 60.91778)", +"http://www.yso.fi/onto/yso/p112932":"POINT(28.87798 62.85726)", +"http://www.yso.fi/onto/yso/p112933":"POINT(21.69969 63.16546)", +"http://www.yso.fi/onto/yso/p112934":"POINT(24.92699 60.26296)", +"http://www.yso.fi/onto/yso/p112935":"", +"http://www.yso.fi/onto/yso/p112936":"", +"http://www.yso.fi/onto/yso/p112937":"POINT(26.98188 60.56988)", +"http://www.yso.fi/onto/yso/p112981":"POINT(22.62689 60.97906)", +"http://www.yso.fi/onto/yso/p113005":"POINT(2.6888888888889 48.315277777778)", +"http://www.yso.fi/onto/yso/p113007":"POINT(23.05853 61.85664)", +"http://www.yso.fi/onto/yso/p113008":"POINT(25.72068 60.35779)", +"http://www.yso.fi/onto/yso/p113009":"POINT(25.40549 60.10819)", +"http://www.yso.fi/onto/yso/p113011":"POINT(113.258976 23.128795)", +"http://www.yso.fi/onto/yso/p113013":"POINT(23.78401 61.51314)", +"http://www.yso.fi/onto/yso/p113014":"POINT(26.1738 65.13736)", +"http://www.yso.fi/onto/yso/p113016":"POINT(23.94197 60.61292)", +"http://www.yso.fi/onto/yso/p113017":"POINT(23.94197 60.61292)", +"http://www.yso.fi/onto/yso/p113030":"POINT(25.9069 61.94431)", +"http://www.yso.fi/onto/yso/p113031":"POINT(28.87865 64.08987)", +"http://www.yso.fi/onto/yso/p113032":"POINT(28.8949 64.09793)", +"http://www.yso.fi/onto/yso/p113033":"POINT(28.75398 64.03665)", +"http://www.yso.fi/onto/yso/p113034":"POINT(23.90227 60.67144)", +"http://www.yso.fi/onto/yso/p113035":"POINT(28.93987 66.884)", +"http://www.yso.fi/onto/yso/p113036":"POINT(28.89569 66.93263)", +"http://www.yso.fi/onto/yso/p113037":"", +"http://www.yso.fi/onto/yso/p113037":"", +"http://www.yso.fi/onto/yso/p113038":"", +"http://www.yso.fi/onto/yso/p113038":"", +"http://www.yso.fi/onto/yso/p113039":"POINT(25.68264 60.99151)", +"http://www.yso.fi/onto/yso/p113040":"POINT(25.60508 60.96923)", +"http://www.yso.fi/onto/yso/p113046":"POINT(21.53848 61.5707)", +"http://www.yso.fi/onto/yso/p113050":"POINT(10.666666666667 45.633333333333)", +"http://www.yso.fi/onto/yso/p113052":"POINT(25.17702 61.0479)", +"http://www.yso.fi/onto/yso/p113053":"POINT(25.17436 61.04108)", +"http://www.yso.fi/onto/yso/p113054":"POINT(20.222778 67.854722)", +"http://www.yso.fi/onto/yso/p113056":"POINT(22.88876 61.35349)", +"http://www.yso.fi/onto/yso/p113060":"POINT(26.49432 60.36491)", +"http://www.yso.fi/onto/yso/p113067":"POINT(29.516666666667 64.125)", +"http://www.yso.fi/onto/yso/p113073":"POINT(22.89595 63.62542)", +"http://www.yso.fi/onto/yso/p113074":"POINT(22.89595 63.62542)", +"http://www.yso.fi/onto/yso/p113075":"", +"http://www.yso.fi/onto/yso/p113076":"POINT(8.54 47.378611111111)", +"http://www.yso.fi/onto/yso/p113077":"POINT(24.53432 60.99655)", +"http://www.yso.fi/onto/yso/p113079":"POINT(44.1 33.1)", +"http://www.yso.fi/onto/yso/p113080":"POINT(24.71527 61.16883)", +"http://www.yso.fi/onto/yso/p113082":"POINT(28.36607 63.13877)", +"http://www.yso.fi/onto/yso/p113083":"POINT(28.18014 63.22821)", +"http://www.yso.fi/onto/yso/p113084":"POINT(29.22556 69.47803)", +"http://www.yso.fi/onto/yso/p113089":"POINT(23.90621 63.12848)", +"http://www.yso.fi/onto/yso/p113092":"POINT(13.818888888889 46.761111111111)", +"http://www.yso.fi/onto/yso/p113093":"POINT(28.858333333333 61.108333333333)", +"http://www.yso.fi/onto/yso/p113095":"POINT(21.81352 61.49071)", +"http://www.yso.fi/onto/yso/p113096":"", +"http://www.yso.fi/onto/yso/p113097":"POINT(28.23446 63.14029)", +"http://www.yso.fi/onto/yso/p113099":"POINT(24.1543 61.55974)", +"http://www.yso.fi/onto/yso/p113100":"POINT(24.18067 61.56229)", +"http://www.yso.fi/onto/yso/p113101":"POINT(27.96348 61.83193)", +"http://www.yso.fi/onto/yso/p113102":"POINT(21.70209 60.62042)", +"http://www.yso.fi/onto/yso/p113110":"POINT(27.80316 63.77865)", +"http://www.yso.fi/onto/yso/p113113":"POINT(25.89813 63.20278)", +"http://www.yso.fi/onto/yso/p113116":"POINT(47.866666666667 56.7)", +"http://www.yso.fi/onto/yso/p113117":"POINT(44.45 54.433333333333)", +"http://www.yso.fi/onto/yso/p113118":"POINT(52.75 57.283333333333)", +"http://www.yso.fi/onto/yso/p113119":"POINT(27.12641 63.21012)", +"http://www.yso.fi/onto/yso/p113121":"POINT(23.06848 60.05646)", +"http://www.yso.fi/onto/yso/p113122":"POINT(26.52074 62.78843)", +"http://www.yso.fi/onto/yso/p113123":"POINT(28.72237 61.08136)", +"http://www.yso.fi/onto/yso/p113124":"POINT(22.13238 61.10775)", +"http://www.yso.fi/onto/yso/p113126":"POINT(111.8 27.4)", +"http://www.yso.fi/onto/yso/p113127":"POINT(22.45549 60.82115)", +"http://www.yso.fi/onto/yso/p113128":"", +"http://www.yso.fi/onto/yso/p113174":"POINT(28.77012 61.16766)", +"http://www.yso.fi/onto/yso/p113238":"POINT(27.28343 61.63121)", +"http://www.yso.fi/onto/yso/p113240":"POINT(9.71 59.115556)", +"http://www.yso.fi/onto/yso/p113242":"POINT(25.55267 67.26936)", +"http://www.yso.fi/onto/yso/p113245":"POINT(21.66923 61.58927)", +"http://www.yso.fi/onto/yso/p113246":"POINT(25.73946 61.9519)", +"http://www.yso.fi/onto/yso/p113254":"", +"http://www.yso.fi/onto/yso/p113261":"POINT(28.92711 62.70105)", +"http://www.yso.fi/onto/yso/p113262":"", +"http://www.yso.fi/onto/yso/p113263":"POINT(27.158333333333 62.3)", +"http://www.yso.fi/onto/yso/p113294":"POINT(24.8384 60.32573)", +"http://www.yso.fi/onto/yso/p113295":"POINT(29.26064 65.9276)", +"http://www.yso.fi/onto/yso/p113296":"POINT(29.65406 65.93197)", +"http://www.yso.fi/onto/yso/p113308":"POINT(25.70957 62.24828)", +"http://www.yso.fi/onto/yso/p113309":"POINT(26.63948 60.83731)", +"http://www.yso.fi/onto/yso/p113310":"POINT(165.3 -21.25)", +"http://www.yso.fi/onto/yso/p113311":"", +"http://www.yso.fi/onto/yso/p113317":"POINT(8.5205555555556 47.951111111111)", +"http://www.yso.fi/onto/yso/p113323":"POINT(22.41489 60.86971)", +"http://www.yso.fi/onto/yso/p113324":"POINT(-93 16)", +"http://www.yso.fi/onto/yso/p113328":"POINT(28.10625 62.18794)", +"http://www.yso.fi/onto/yso/p113329":"POINT(27.46277 62.12695)", +"http://www.yso.fi/onto/yso/p113330":"POINT(27.5227 62.08044)", +"http://www.yso.fi/onto/yso/p113331":"POINT(27.41312 62.06849)", +"http://www.yso.fi/onto/yso/p113336":"POINT(22.34444 61.05127)", +"http://www.yso.fi/onto/yso/p113337":"POINT(21.36384 62.39264)", +"http://www.yso.fi/onto/yso/p113339":"POINT(24.54184 61.55751)", +"http://www.yso.fi/onto/yso/p113345":"POINT(26.69205 61.79318)", +"http://www.yso.fi/onto/yso/p113346":"", +"http://www.yso.fi/onto/yso/p113349":"POINT(27.20192 66.086)", +"http://www.yso.fi/onto/yso/p113350":"POINT(27.207 66.04933333)", +"http://www.yso.fi/onto/yso/p113353":"POINT(24.79097 60.73638)", +"http://www.yso.fi/onto/yso/p113366":"POINT(24.948771 60.16898)", +"http://www.yso.fi/onto/yso/p113368":"POINT(22.77057 61.17194)", +"http://www.yso.fi/onto/yso/p113435":"POINT(29.366666666667 60.466666666667)", +"http://www.yso.fi/onto/yso/p113438":"POINT(26.34404 63.21034)", +"http://www.yso.fi/onto/yso/p113439":"POINT(26.31994 63.25212)", +"http://www.yso.fi/onto/yso/p113441":"POINT(24.9265 60.15163)", +"http://www.yso.fi/onto/yso/p113442":"POINT(23.75855 60.12649)", +"http://www.yso.fi/onto/yso/p113443":"POINT(21.59472 61.32272)", +"http://www.yso.fi/onto/yso/p113444":"POINT(21.80813 60.9442)", +"http://www.yso.fi/onto/yso/p113445":"POINT(21.76445 60.25253)", +"http://www.yso.fi/onto/yso/p113446":"POINT(23.4337 63.94387)", +"http://www.yso.fi/onto/yso/p113447":"POINT(22.98707 62.98128)", +"http://www.yso.fi/onto/yso/p113448":"POINT(25.68605 65.31006)", +"http://www.yso.fi/onto/yso/p113449":"POINT(23.75998 66.2282)", +"http://www.yso.fi/onto/yso/p113450":"POINT(27.61449 67.90884)", +"http://www.yso.fi/onto/yso/p113451":"POINT(28.6935 65.89004)", +"http://www.yso.fi/onto/yso/p113452":"POINT(25.34181 61.22287)", +"http://www.yso.fi/onto/yso/p113453":"POINT(25.34779 61.93762)", +"http://www.yso.fi/onto/yso/p113454":"POINT(29.23601 62.64667)", +"http://www.yso.fi/onto/yso/p113455":"POINT(26.76234 60.35891)", +"http://www.yso.fi/onto/yso/p113490":"", +"http://www.yso.fi/onto/yso/p113492":"POINT(25.60834 62.03776)", +"http://www.yso.fi/onto/yso/p113493":"POINT(25.60546 62.02146)", +"http://www.yso.fi/onto/yso/p113494":"POINT(25.60285 62.02423)", +"http://www.yso.fi/onto/yso/p113495":"POINT(25.17163 61.8104)", +"http://www.yso.fi/onto/yso/p113496":"POINT(25.09772 61.83997)", +"http://www.yso.fi/onto/yso/p113497":"POINT(22.39477 61.45108)", +"http://www.yso.fi/onto/yso/p113498":"POINT(23.75291 61.42892)", +"http://www.yso.fi/onto/yso/p113499":"POINT(25.39874 60.59351)", +"http://www.yso.fi/onto/yso/p113500":"POINT(26.68345 62.69624)", +"http://www.yso.fi/onto/yso/p113501":"POINT(23.97286 63.22207)", +"http://www.yso.fi/onto/yso/p113503":"POINT(21.83583 61.47351)", +"http://www.yso.fi/onto/yso/p113508":"POINT(121.13333333333 5.9833333333333)", +"http://www.yso.fi/onto/yso/p113510":"POINT(29.91525 62.74388)", +"http://www.yso.fi/onto/yso/p113523":"POINT(21.99434 68.78933)", +"http://www.yso.fi/onto/yso/p113524":"POINT(23.76511 59.93309)", +"http://www.yso.fi/onto/yso/p113526":"POINT(23.4718 62.82135)", +"http://www.yso.fi/onto/yso/p113527":"POINT(21.42502 62.18264)", +"http://www.yso.fi/onto/yso/p113531":"POINT(30.2 61.516666666667)", +"http://www.yso.fi/onto/yso/p113534":"POINT(29.86147 62.62229)", +"http://www.yso.fi/onto/yso/p113536":"POINT(28.01503 65.04114)", +"http://www.yso.fi/onto/yso/p113538":"", +"http://www.yso.fi/onto/yso/p113539":"POINT(25.46424 65.02593)", +"http://www.yso.fi/onto/yso/p113541":"POINT(21.42421 62.16449)", +"http://www.yso.fi/onto/yso/p113543":"POINT(25.36742 66.62587)", +"http://www.yso.fi/onto/yso/p113544":"POINT(25.34085 66.63009)", +"http://www.yso.fi/onto/yso/p113552":"POINT(24.50034 62.14424)", +"http://www.yso.fi/onto/yso/p113564":"POINT(27.03538 60.3944)", +"http://www.yso.fi/onto/yso/p113565":"POINT(26.96287 60.37281)", +"http://www.yso.fi/onto/yso/p113566":"POINT(23.52041 59.91135)", +"http://www.yso.fi/onto/yso/p113567":"POINT(27.14248 60.28935)", +"http://www.yso.fi/onto/yso/p113568":"POINT(127 37.583333333333)", +"http://www.yso.fi/onto/yso/p113641":"POINT(23.30406 63.2692)", +"http://www.yso.fi/onto/yso/p113646":"POINT(25.8481 61.43045)", +"http://www.yso.fi/onto/yso/p113647":"POINT(22.18611 61.21751)", +"http://www.yso.fi/onto/yso/p113648":"POINT(25.9155 67.29732)", +"http://www.yso.fi/onto/yso/p113649":"POINT(25.91882 67.27929)", +"http://www.yso.fi/onto/yso/p113651":"POINT(52.4 29.9)", +"http://www.yso.fi/onto/yso/p113654":"POINT(22.28905 60.4857)", +"http://www.yso.fi/onto/yso/p113655":"POINT(25.71411 62.22666)", +"http://www.yso.fi/onto/yso/p113656":"POINT(24.0588 62.06768)", +"http://www.yso.fi/onto/yso/p113663":"", +"http://www.yso.fi/onto/yso/p113664":"POINT(25.46854 62.35458)", +"http://www.yso.fi/onto/yso/p113666":"POINT(24.82649 61.86299)", +"http://www.yso.fi/onto/yso/p113712":"POINT(30.84155 62.34004)", +"http://www.yso.fi/onto/yso/p113715":"POINT(23.93894 61.54058)", +"http://www.yso.fi/onto/yso/p113716":"POINT(23.93088 61.55573)", +"http://www.yso.fi/onto/yso/p113721":"", +"http://www.yso.fi/onto/yso/p113724":"POINT(26.14049 63.66246)", +"http://www.yso.fi/onto/yso/p113725":"POINT(26.14049 63.66246)", +"http://www.yso.fi/onto/yso/p113726":"POINT(25.84045 60.29192)", +"http://www.yso.fi/onto/yso/p113728":"POINT(23.31151 60.72724)", +"http://www.yso.fi/onto/yso/p113730":"POINT(28.775254 60.527102)", +"http://www.yso.fi/onto/yso/p113736":"POINT(22.85906 60.90843)", +"http://www.yso.fi/onto/yso/p113737":"POINT(23.0708 60.39514)", +"http://www.yso.fi/onto/yso/p113738":"POINT(23.01421 60.34289)", +"http://www.yso.fi/onto/yso/p113785":"POINT(22.95301 60.25707)", +"http://www.yso.fi/onto/yso/p113788":"POINT(22.5224 61.58282)", +"http://www.yso.fi/onto/yso/p113789":"POINT(22.52137 61.75063)", +"http://www.yso.fi/onto/yso/p113790":"POINT(24.9198 60.1925)", +"http://www.yso.fi/onto/yso/p113792":"POINT(22.03438 61.54064)", +"http://www.yso.fi/onto/yso/p113793":"POINT(22.07818 61.49469)", +"http://www.yso.fi/onto/yso/p113796":"POINT(22.39917 61.01935)", +"http://www.yso.fi/onto/yso/p113797":"POINT(26.59723 60.9317)", +"http://www.yso.fi/onto/yso/p113799":"POINT(22.68746 60.48794)", +"http://www.yso.fi/onto/yso/p113800":"POINT(22.87677 60.8592)", +"http://www.yso.fi/onto/yso/p113801":"POINT(22.29151 61.65994)", +"http://www.yso.fi/onto/yso/p113802":"POINT(24.48064 61.63648)", +"http://www.yso.fi/onto/yso/p113803":"POINT(22.90982 61.13907)", +"http://www.yso.fi/onto/yso/p113804":"POINT(23.34369 61.4846)", +"http://www.yso.fi/onto/yso/p113805":"POINT(24.68449 60.60459)", +"http://www.yso.fi/onto/yso/p113806":"POINT(22.45165 61.50765)", +"http://www.yso.fi/onto/yso/p113807":"POINT(22.52423 61.74222)", +"http://www.yso.fi/onto/yso/p113808":"POINT(22.62295 60.88871)", +"http://www.yso.fi/onto/yso/p113809":"POINT(22.58061 60.88929)", +"http://www.yso.fi/onto/yso/p115076":"POINT(24.69356 60.31723)", +"http://www.yso.fi/onto/yso/p115077":"POINT(23.00877 63.79613)", +"http://www.yso.fi/onto/yso/p115079":"POINT(23.75281 61.48487)", +"http://www.yso.fi/onto/yso/p115080":"POINT(23.80046 61.49897)", +"http://www.yso.fi/onto/yso/p115081":"POINT(21.98977 60.44166)", +"http://www.yso.fi/onto/yso/p115082":"POINT(24.19612 59.93332)", +"http://www.yso.fi/onto/yso/p115084":"POINT(25.6837 61.50377)", +"http://www.yso.fi/onto/yso/p115085":"POINT(21.86559 60.57118)", +"http://www.yso.fi/onto/yso/p115086":"POINT(22.15688 61.01367)", +"http://www.yso.fi/onto/yso/p115087":"POINT(24.28285 60.22595)", +"http://www.yso.fi/onto/yso/p115088":"POINT(10 46)", +"http://www.yso.fi/onto/yso/p115187":"POINT(24.48696 61.00759)", +"http://www.yso.fi/onto/yso/p115267":"POINT(-8.5333333333333 42.883333333333)", +"http://www.yso.fi/onto/yso/p115290":"POINT(23.69281 63.00436)", +"http://www.yso.fi/onto/yso/p115292":"POINT(23.64018 63.0233)", +"http://www.yso.fi/onto/yso/p115294":"POINT(24.74241 60.17641)", +"http://www.yso.fi/onto/yso/p115295":"POINT(24.28928 63.71805)", +"http://www.yso.fi/onto/yso/p115296":"POINT(22.50713 61.32305)", +"http://www.yso.fi/onto/yso/p115303":"POINT(22.49891 59.72313)", +"http://www.yso.fi/onto/yso/p115307":"POINT(26.63281 61.06052)", +"http://www.yso.fi/onto/yso/p115319":"POINT(21.42581 62.75543)", +"http://www.yso.fi/onto/yso/p115336":"POINT(25.07014 60.31792)", +"http://www.yso.fi/onto/yso/p115340":"POINT(-43.196388888889 -22.908333333333)", +"http://www.yso.fi/onto/yso/p115344":"POINT(-118.33333333333 34.1)", +"http://www.yso.fi/onto/yso/p115345":"POINT(76.972 8.5074)", +"http://www.yso.fi/onto/yso/p115347":"POINT(23.68863 61.52927)", +"http://www.yso.fi/onto/yso/p115348":"POINT(24.75102 60.26533)", +"http://www.yso.fi/onto/yso/p115446":"POINT(24.88414 60.18112)", +"http://www.yso.fi/onto/yso/p115519":"POINT(27.1113 63.23045)", +"http://www.yso.fi/onto/yso/p115588":"POINT(26.48428 62.34066)", +"http://www.yso.fi/onto/yso/p115590":"POINT(30.70018 63.16647)", +"http://www.yso.fi/onto/yso/p115591":"POINT(20.871666666667 63.798888888889)", +"http://www.yso.fi/onto/yso/p115592":"POINT(25.8374 63.64143)", +"http://www.yso.fi/onto/yso/p115593":"POINT(21.17944 62.7215)", +"http://www.yso.fi/onto/yso/p115594":"POINT(28.15181 60.90788)", +"http://www.yso.fi/onto/yso/p115595":"POINT(-7.02 57.76)", +"http://www.yso.fi/onto/yso/p115606":"POINT(2.8333333333333 49.5)", +"http://www.yso.fi/onto/yso/p115608":"POINT(28.15181 60.90788)", +"http://www.yso.fi/onto/yso/p115616":"POINT(25.70383 61.79681)", +"http://www.yso.fi/onto/yso/p115692":"POINT(3.4 6.45)", +"http://www.yso.fi/onto/yso/p115745":"POINT(23.71003 62.15774)", +"http://www.yso.fi/onto/yso/p115746":"POINT(23.7234 62.29658)", +"http://www.yso.fi/onto/yso/p115750":"POINT(23.54879 62.32519)", +"http://www.yso.fi/onto/yso/p115754":"POINT(27.26847 63.54004)", +"http://www.yso.fi/onto/yso/p115785":"POINT(23.59397 62.55048)", +"http://www.yso.fi/onto/yso/p115788":"POINT(24.48617 61.20109)", +"http://www.yso.fi/onto/yso/p115790":"POINT(25.4358 60.37091)", +"http://www.yso.fi/onto/yso/p115946":"POINT(24.95573 60.15594)", +"http://www.yso.fi/onto/yso/p115947":"POINT(25.06671 60.19227)", +"http://www.yso.fi/onto/yso/p115949":"POINT(25.34921 63.06739)", +"http://www.yso.fi/onto/yso/p115950":"POINT(-87.65 41.9)", +"http://www.yso.fi/onto/yso/p115951":"POINT(24.82897 60.64832)", +"http://www.yso.fi/onto/yso/p115952":"POINT(24.83534 60.64382)", +"http://www.yso.fi/onto/yso/p115953":"POINT(23.73542 61.74452)", +"http://www.yso.fi/onto/yso/p115954":"POINT(-3.1988888888889 55.95)", +"http://www.yso.fi/onto/yso/p115959":"POINT(21.73601 62.09259)", +"http://www.yso.fi/onto/yso/p115991":"POINT(29.13388889 60.24166667)", +"http://www.yso.fi/onto/yso/p116017":"POINT(23.981944444444 40.326944444444)", +"http://www.yso.fi/onto/yso/p116045":"POINT(29.563333333333 60.204444444444)", +"http://www.yso.fi/onto/yso/p116064":"POINT(27.26857 61.70247)", +"http://www.yso.fi/onto/yso/p116117":"POINT(27.642 62.90364)", +"http://www.yso.fi/onto/yso/p116119":"POINT(23.63907 61.498)", +"http://www.yso.fi/onto/yso/p116221":"POINT(28.22656 62.30045)", +"http://www.yso.fi/onto/yso/p116312":"POINT(-111.656944 34.286667)", +"http://www.yso.fi/onto/yso/p116325":"POINT(22.54524 60.26944)", +"http://www.yso.fi/onto/yso/p116332":"POINT(35.5 31.5)", +"http://www.yso.fi/onto/yso/p116380":"POINT(37.5 65.5)", +"http://www.yso.fi/onto/yso/p116389":"POINT(0 69)", +"http://www.yso.fi/onto/yso/p116418":"POINT(27.4424 68.43342)", +"http://www.yso.fi/onto/yso/p116420":"POINT(27.50273 68.34131)", +"http://www.yso.fi/onto/yso/p116422":"POINT(28.79272 66.77089)", +"http://www.yso.fi/onto/yso/p116423":"POINT(23.05235 61.4016)", +"http://www.yso.fi/onto/yso/p116424":"POINT(28.01412 63.29069)", +"http://www.yso.fi/onto/yso/p116425":"POINT(29.33465 65.27029)", +"http://www.yso.fi/onto/yso/p116428":"POINT(24.50599 60.37514)", +"http://www.yso.fi/onto/yso/p116431":"POINT(24.13217 67.94529)", +"http://www.yso.fi/onto/yso/p116432":"POINT(28.96062 66.25954)", +"http://www.yso.fi/onto/yso/p116436":"POINT(27.55588 68.23467)", +"http://www.yso.fi/onto/yso/p116441":"POINT(21.43857 62.92822)", +"http://www.yso.fi/onto/yso/p116443":"POINT(37.359166666667 -3.0666666666667)", +"http://www.yso.fi/onto/yso/p116444":"POINT(32.25 26.05)", +"http://www.yso.fi/onto/yso/p116445":"POINT(86.925277777778 27.988055555556)", +"http://www.yso.fi/onto/yso/p116446":"POINT(29.91315 63.00339)", +"http://www.yso.fi/onto/yso/p116447":"POINT(25.273 60.86801)", +"http://www.yso.fi/onto/yso/p116448":"POINT(24.71932 60.16624)", +"http://www.yso.fi/onto/yso/p116449":"POINT(21.66875 60.85569)", +"http://www.yso.fi/onto/yso/p116450":"POINT(23.15731 60.39933)", +"http://www.yso.fi/onto/yso/p116451":"POINT(21.9179 60.54607)", +"http://www.yso.fi/onto/yso/p116453":"POINT(24.84466667 60.20947222)", +"http://www.yso.fi/onto/yso/p116454":"POINT(25.17476 65.57843)", +"http://www.yso.fi/onto/yso/p116457":"POINT(26.17525 62.38052)", +"http://www.yso.fi/onto/yso/p116460":"", +"http://www.yso.fi/onto/yso/p116465":"POINT(30.0125 60.147222222222)", +"http://www.yso.fi/onto/yso/p116466":"", +"http://www.yso.fi/onto/yso/p116590":"POINT(27.33284 63.07327)", +"http://www.yso.fi/onto/yso/p116650":"POINT(25.55946 60.37882)", +"http://www.yso.fi/onto/yso/p116860":"POINT(-90 33)", +"http://www.yso.fi/onto/yso/p116862":"POINT(27.79855 63.00253)", +"http://www.yso.fi/onto/yso/p116931":"POINT(25.14339 60.56729)", +"http://www.yso.fi/onto/yso/p116933":"POINT(29.0689 65.2923)", +"http://www.yso.fi/onto/yso/p116934":"POINT(24.93705 60.15645)", +"http://www.yso.fi/onto/yso/p116935":"POINT(24.94743 60.1586)", +"http://www.yso.fi/onto/yso/p116942":"POINT(29.366666666667 61.296944444444)", +"http://www.yso.fi/onto/yso/p116948":"POINT(23.02589 60.97744)", +"http://www.yso.fi/onto/yso/p116949":"", +"http://www.yso.fi/onto/yso/p116953":"POINT(25.04918 60.32184)", +"http://www.yso.fi/onto/yso/p116954":"POINT(-125 54)", +"http://www.yso.fi/onto/yso/p117002":"POINT(6.8166666666667 51.216666666667)", +"http://www.yso.fi/onto/yso/p117024":"POINT(28.51379 62.40638)", +"http://www.yso.fi/onto/yso/p117025":"POINT(28.52261 62.38558)", +"http://www.yso.fi/onto/yso/p117131":"POINT(16.35 48.2)", +"http://www.yso.fi/onto/yso/p117139":"POINT(-77.036666666667 38.895)", +"http://www.yso.fi/onto/yso/p117179":"POINT(21.36213 61.13827)", +"http://www.yso.fi/onto/yso/p117180":"POINT(21.3025 61.14512)", +"http://www.yso.fi/onto/yso/p117181":"POINT(25.33791 64.07537)", +"http://www.yso.fi/onto/yso/p117182":"POINT(25.86243 64.35831)", +"http://www.yso.fi/onto/yso/p117183":"POINT(24.0615 64.17579)", +"http://www.yso.fi/onto/yso/p117184":"POINT(23.73135 64.20811)", +"http://www.yso.fi/onto/yso/p117189":"POINT(25.90265 62.16101)", +"http://www.yso.fi/onto/yso/p117190":"POINT(129.16666666667 66.4)", +"http://www.yso.fi/onto/yso/p117191":"POINT(24.84774 60.23716)", +"http://www.yso.fi/onto/yso/p117193":"POINT(30.1462 62.30944)", +"http://www.yso.fi/onto/yso/p117194":"POINT(28.08749 60.9793)", +"http://www.yso.fi/onto/yso/p117215":"", +"http://www.yso.fi/onto/yso/p117275":"POINT(26.55183 61.5251)", +"http://www.yso.fi/onto/yso/p117764":"", +"http://www.yso.fi/onto/yso/p117766":"POINT(21.416666666667 54.8)", +"http://www.yso.fi/onto/yso/p117769":"POINT(23.42302 63.02809)", +"http://www.yso.fi/onto/yso/p117775":"POINT(23.89225 61.54537)", +"http://www.yso.fi/onto/yso/p117776":"POINT(23.84679 61.67359)", +"http://www.yso.fi/onto/yso/p117778":"POINT(27.44855 67.19064)", +"http://www.yso.fi/onto/yso/p117779":"POINT(27.95575 62.77253)", +"http://www.yso.fi/onto/yso/p117780":"POINT(25.95942 61.81345)", +"http://www.yso.fi/onto/yso/p117781":"POINT(21.17059 62.65488)", +"http://www.yso.fi/onto/yso/p117782":"POINT(26.72264 63.7389)", +"http://www.yso.fi/onto/yso/p117783":"POINT(26.74382 63.71748)", +"http://www.yso.fi/onto/yso/p117784":"POINT(25.05421 60.27398)", +"http://www.yso.fi/onto/yso/p117785":"POINT(25.07379 60.26872)", +"http://www.yso.fi/onto/yso/p117788":"POINT(29.0091 70.0911)", +"http://www.yso.fi/onto/yso/p118198":"POINT(23.51576 62.76646)", +"http://www.yso.fi/onto/yso/p118199":"POINT(26.69862 64.84769)", +"http://www.yso.fi/onto/yso/p118208":"POINT(23.35337 61.47764)", +"http://www.yso.fi/onto/yso/p118209":"", +"http://www.yso.fi/onto/yso/p118210":"POINT(3 12)", +"http://www.yso.fi/onto/yso/p118211":"POINT(25.01086 60.28366)", +"http://www.yso.fi/onto/yso/p118212":"POINT(24.62078 60.1959)", +"http://www.yso.fi/onto/yso/p118213":"", +"http://www.yso.fi/onto/yso/p118215":"POINT(27.91075 64.66197)", +"http://www.yso.fi/onto/yso/p118216":"POINT(24.0522 62.26963)", +"http://www.yso.fi/onto/yso/p118217":"POINT(28.65202 62.78459)", +"http://www.yso.fi/onto/yso/p118218":"POINT(27.04146 62.0456)", +"http://www.yso.fi/onto/yso/p118219":"POINT(25.99397 60.39531)", +"http://www.yso.fi/onto/yso/p118220":"POINT(24.69066 60.26641)", +"http://www.yso.fi/onto/yso/p118221":"POINT(24.65151 60.24099)", +"http://www.yso.fi/onto/yso/p118222":"POINT(24.65684 60.27356)", +"http://www.yso.fi/onto/yso/p118223":"POINT(24.66989 60.28014)", +"http://www.yso.fi/onto/yso/p118224":"POINT(151.20833333333 -33.869444444444)", +"http://www.yso.fi/onto/yso/p118225":"POINT(23.00149 61.75894)", +"http://www.yso.fi/onto/yso/p118235":"POINT(23.72409 60.81874)", +"http://www.yso.fi/onto/yso/p118238":"POINT(24.30722 64.13141)", +"http://www.yso.fi/onto/yso/p118251":"", +"http://www.yso.fi/onto/yso/p118301":"POINT(20.466666666667 44.816666666667)", +"http://www.yso.fi/onto/yso/p118321":"POINT(26.74943 62.31252)", +"http://www.yso.fi/onto/yso/p118322":"POINT(26.73874 62.2783)", +"http://www.yso.fi/onto/yso/p118393":"POINT(26.19585 61.37632)", +"http://www.yso.fi/onto/yso/p118402":"POINT(28.888611111111 60.661944444444)", +"http://www.yso.fi/onto/yso/p118494":"POINT(23.48646 61.01979)", +"http://www.yso.fi/onto/yso/p118588":"POINT(25.63277 63.31064)", +"http://www.yso.fi/onto/yso/p118590":"POINT(25.33648 61.28315)", +"http://www.yso.fi/onto/yso/p118594":"POINT(21.28106 63.18103)", +"http://www.yso.fi/onto/yso/p118596":"POINT(22.35882 63.38989)", +"http://www.yso.fi/onto/yso/p118597":"POINT(25.41 64.23885)", +"http://www.yso.fi/onto/yso/p118598":"POINT(69.165833333333 34.532777777778)", +"http://www.yso.fi/onto/yso/p118599":"POINT(22.37818 61.91698)", +"http://www.yso.fi/onto/yso/p118600":"POINT(27.04403 62.66368)", +"http://www.yso.fi/onto/yso/p118601":"POINT(26.83296 62.81091)", +"http://www.yso.fi/onto/yso/p118602":"POINT(26.96662 60.82821)", +"http://www.yso.fi/onto/yso/p118603":"POINT(14.733333333333 60.866666666667)", +"http://www.yso.fi/onto/yso/p118604":"POINT(13.804166666667 45.636111111111)", +"http://www.yso.fi/onto/yso/p118605":"POINT(7.7 45.066666666667)", +"http://www.yso.fi/onto/yso/p118607":"POINT(11.33333334 46.50000001)", +"http://www.yso.fi/onto/yso/p118609":"POINT(11.4 47.266666666667)", +"http://www.yso.fi/onto/yso/p118610":"POINT(15.426111 42.775833)", +"http://www.yso.fi/onto/yso/p118611":"POINT(13 45.316666666667)", +"http://www.yso.fi/onto/yso/p118612":"POINT(28.94768 63.12222)", +"http://www.yso.fi/onto/yso/p118614":"POINT(21.60854 61.5465)", +"http://www.yso.fi/onto/yso/p118615":"POINT(23.66246 60.35688)", +"http://www.yso.fi/onto/yso/p118617":"", +"http://www.yso.fi/onto/yso/p118619":"", +"http://www.yso.fi/onto/yso/p118655":"POINT(25.56287 61.70503)", +"http://www.yso.fi/onto/yso/p118667":"POINT(22.35882 63.38989)", +"http://www.yso.fi/onto/yso/p118915":"POINT(28.54006 64.24953)", +"http://www.yso.fi/onto/yso/p118916":"POINT(28.5086 64.31228)", +"http://www.yso.fi/onto/yso/p118917":"POINT(28.51836 64.32887)", +"http://www.yso.fi/onto/yso/p118918":"POINT(24.45938 64.1981)", +"http://www.yso.fi/onto/yso/p118919":"POINT(22.95519 60.32989)", +"http://www.yso.fi/onto/yso/p118920":"POINT(22.89657 60.31372)", +"http://www.yso.fi/onto/yso/p118921":"POINT(22.82929 60.29165)", +"http://www.yso.fi/onto/yso/p118922":"POINT(22.66482 60.30135)", +"http://www.yso.fi/onto/yso/p118924":"POINT(22.64036 60.3337)", +"http://www.yso.fi/onto/yso/p118925":"POINT(22.56222 60.4601)", +"http://www.yso.fi/onto/yso/p118926":"POINT(22.56222 60.4601)", +"http://www.yso.fi/onto/yso/p118927":"POINT(22.47079 60.43015)", +"http://www.yso.fi/onto/yso/p118929":"POINT(22.45564 60.08647)", +"http://www.yso.fi/onto/yso/p118987":"POINT(24.91029 60.20147)", +"http://www.yso.fi/onto/yso/p118989":"POINT(24.85166 60.18004)", +"http://www.yso.fi/onto/yso/p119045":"", +"http://www.yso.fi/onto/yso/p119045":"", +"http://www.yso.fi/onto/yso/p119093":"POINT(29.74818 61.72384)", +"http://www.yso.fi/onto/yso/p119163":"POINT(23.60731 63.37607)", +"http://www.yso.fi/onto/yso/p119188":"POINT(77.216666666667 28.666666666667)", +"http://www.yso.fi/onto/yso/p119201":"POINT(25.52718 61.1203)", +"http://www.yso.fi/onto/yso/p119237":"POINT(28.48124 67.65208)", +"http://www.yso.fi/onto/yso/p119238":"POINT(28.47348 67.46797)", +"http://www.yso.fi/onto/yso/p119273":"POINT(23.1278 60.59813)", +"http://www.yso.fi/onto/yso/p119275":"POINT(25.8567 61.55602)", +"http://www.yso.fi/onto/yso/p119276":"POINT(25.83996 61.53209)", +"http://www.yso.fi/onto/yso/p119277":"POINT(25.79466 62.24653)", +"http://www.yso.fi/onto/yso/p119314":"POINT(23.62539 63.43561)", +"http://www.yso.fi/onto/yso/p119315":"POINT(25.63619 60.41872)", +"http://www.yso.fi/onto/yso/p119400":"POINT(18.740277777778 47.785555555556)", +"http://www.yso.fi/onto/yso/p119401":"POINT(18.983333333333 47.783333333333)", +"http://www.yso.fi/onto/yso/p119402":"POINT(18.416666666667 47.2)", +"http://www.yso.fi/onto/yso/p119403":"POINT(17.916666666667 47.1)", +"http://www.yso.fi/onto/yso/p119404":"POINT(24.02449 68.28758)", +"http://www.yso.fi/onto/yso/p119405":"POINT(25.01136 60.25172)", +"http://www.yso.fi/onto/yso/p119406":"POINT(28.773333333333 60.925)", +"http://www.yso.fi/onto/yso/p119407":"", +"http://www.yso.fi/onto/yso/p119408":"POINT(21.21241 60.60264)", +"http://www.yso.fi/onto/yso/p119409":"POINT(23.92904 60.86351)", +"http://www.yso.fi/onto/yso/p119410":"", +"http://www.yso.fi/onto/yso/p119411":"POINT(22.42478 59.96142)", +"http://www.yso.fi/onto/yso/p119412":"POINT(21.66113 61.81473)", +"http://www.yso.fi/onto/yso/p119413":"POINT(22.35787 59.95154)", +"http://www.yso.fi/onto/yso/p119415":"POINT(21.60346 59.95109)", +"http://www.yso.fi/onto/yso/p119419":"POINT(25.04434 60.14563)", +"http://www.yso.fi/onto/yso/p119676":"POINT(24.95228 60.26109)", +"http://www.yso.fi/onto/yso/p119734":"POINT(21.85397 61.46304)", +"http://www.yso.fi/onto/yso/p119843":"POINT(28.9875 60.802777777778)", +"http://www.yso.fi/onto/yso/p119844":"POINT(26.89914 61.93168)", +"http://www.yso.fi/onto/yso/p119930":"POINT(22.24365 60.47305)", +"http://www.yso.fi/onto/yso/p119947":"POINT(27.01884 62.88251)", +"http://www.yso.fi/onto/yso/p119949":"POINT(28.62153 64.36519)", +"http://www.yso.fi/onto/yso/p119951":"POINT(26.94308 60.48569)", +"http://www.yso.fi/onto/yso/p119952":"", +"http://www.yso.fi/onto/yso/p119952":"POINT(24.54444444 65.80138889)", +"http://www.yso.fi/onto/yso/p119965":"POINT(22.39622 62.96671)", +"http://www.yso.fi/onto/yso/p119981":"POINT(24.86069 61.60179)", +"http://www.yso.fi/onto/yso/p119999":"POINT(30.810555555556 63.833611111111)", +"http://www.yso.fi/onto/yso/p120056":"POINT(23.63909 62.37643)", +"http://www.yso.fi/onto/yso/p120316":"POINT(29.97081 63.15022)", +"http://www.yso.fi/onto/yso/p120317":"POINT(27.15661 62.8563)", +"http://www.yso.fi/onto/yso/p120318":"POINT(27.90401 60.63665)", +"http://www.yso.fi/onto/yso/p120319":"POINT(30.366666666667 60.383333333333)", +"http://www.yso.fi/onto/yso/p120320":"POINT(23.63741 62.63792)", +"http://www.yso.fi/onto/yso/p120321":"POINT(28.32707 65.89707)", +"http://www.yso.fi/onto/yso/p120324":"POINT(23.94147 60.4145)", +"http://www.yso.fi/onto/yso/p120326":"POINT(26.83709 67.34077)", +"http://www.yso.fi/onto/yso/p120327":"POINT(24.1145 66.89764)", +"http://www.yso.fi/onto/yso/p120329":"POINT(26.8947 60.87099)", +"http://www.yso.fi/onto/yso/p120331":"POINT(25.59141 60.76586)", +"http://www.yso.fi/onto/yso/p120333":"POINT(28.98776 64.09615)", +"http://www.yso.fi/onto/yso/p120334":"POINT(24.09014 61.19418)", +"http://www.yso.fi/onto/yso/p120336":"POINT(24.09014 61.19418)", +"http://www.yso.fi/onto/yso/p120338":"POINT(-169 66)", +"http://www.yso.fi/onto/yso/p120339":"POINT(24.05236 60.61421)", +"http://www.yso.fi/onto/yso/p120340":"POINT(24.07155 60.61425)", +"http://www.yso.fi/onto/yso/p120538":"POINT(28.71063 61.60585)", +"http://www.yso.fi/onto/yso/p120558":"POINT(21.41219 60.82811)", +"http://www.yso.fi/onto/yso/p120559":"POINT(21.69798 60.94549)", +"http://www.yso.fi/onto/yso/p120560":"POINT(21.72217 60.72463)", +"http://www.yso.fi/onto/yso/p120561":"POINT(21.73736 60.72916)", +"http://www.yso.fi/onto/yso/p120565":"POINT(21.81 60.72841)", +"http://www.yso.fi/onto/yso/p120567":"POINT(23.64309 64.21823)", +"http://www.yso.fi/onto/yso/p120568":"POINT(17.833333333333 59.433333333333)", +"http://www.yso.fi/onto/yso/p120569":"POINT(14.783333333333 56.883333333333)", +"http://www.yso.fi/onto/yso/p120570":"POINT(24.04618 61.09906)", +"http://www.yso.fi/onto/yso/p120572":"POINT(20.9 45.7)", +"http://www.yso.fi/onto/yso/p120573":"POINT(24.69098 62.4003)", +"http://www.yso.fi/onto/yso/p120647":"POINT(22.17092 60.92483)", +"http://www.yso.fi/onto/yso/p120658":"POINT(24.0179 67.35776)", +"http://www.yso.fi/onto/yso/p120659":"POINT(28.69942 66.21999)", +"http://www.yso.fi/onto/yso/p120660":"POINT(27.97963 66.68315)", +"http://www.yso.fi/onto/yso/p120781":"", +"http://www.yso.fi/onto/yso/p120932":"POINT(25.03806111 60.20917222)", +"http://www.yso.fi/onto/yso/p120933":"POINT(-17.133333333333 28.1)", +"http://www.yso.fi/onto/yso/p120935":"POINT(24.98375 60.64282)", +"http://www.yso.fi/onto/yso/p120936":"POINT(22.79671 63.5997)", +"http://www.yso.fi/onto/yso/p120937":"POINT(27.81839 61.12233)", +"http://www.yso.fi/onto/yso/p120938":"POINT(9.190336 45.464161)", +"http://www.yso.fi/onto/yso/p120941":"POINT(24.71318 65.99928)", +"http://www.yso.fi/onto/yso/p121118":"POINT(27.29835 61.69224)", +"http://www.yso.fi/onto/yso/p121213":"POINT(22.78527 61.5581)", +"http://www.yso.fi/onto/yso/p121313":"POINT(26.01214 60.77927)", +"http://www.yso.fi/onto/yso/p121319":"POINT(29.30049 65.27842)", +"http://www.yso.fi/onto/yso/p121321":"POINT(26.05053 62.15388)", +"http://www.yso.fi/onto/yso/p121322":"POINT(22.43321 60.03427)", +"http://www.yso.fi/onto/yso/p121325":"POINT(25.5311 60.30963)", +"http://www.yso.fi/onto/yso/p121326":"POINT(25.94226 62.45834)", +"http://www.yso.fi/onto/yso/p121327":"POINT(24.11905 60.93857)", +"http://www.yso.fi/onto/yso/p121328":"POINT(25.77259 62.23919)", +"http://www.yso.fi/onto/yso/p121329":"POINT(30.05655 65.72973)", +"http://www.yso.fi/onto/yso/p121336":"POINT(24.44428 60.75632)", +"http://www.yso.fi/onto/yso/p121337":"POINT(24.48338 60.74221)", +"http://www.yso.fi/onto/yso/p121338":"POINT(21.62943 61.56823)", +"http://www.yso.fi/onto/yso/p121345":"POINT(22.27583333 60.45083333)", +"http://www.yso.fi/onto/yso/p121600":"POINT(25.18809 61.38471)", +"http://www.yso.fi/onto/yso/p121607":"POINT(26.43022 61.99065)", +"http://www.yso.fi/onto/yso/p121636":"POINT(24.39061 62.34459)", +"http://www.yso.fi/onto/yso/p121637":"POINT(24.27668 62.3608)", +"http://www.yso.fi/onto/yso/p121808":"POINT(24.07598 60.08281)", +"http://www.yso.fi/onto/yso/p121810":"POINT(10.319444444444 49.070833333333)", +"http://www.yso.fi/onto/yso/p121811":"POINT(23.20627 61.92487)", +"http://www.yso.fi/onto/yso/p121812":"POINT(23.24083 61.92613)", +"http://www.yso.fi/onto/yso/p121813":"POINT(23.91803 61.4781)", +"http://www.yso.fi/onto/yso/p121814":"POINT(26.24022 60.5547)", +"http://www.yso.fi/onto/yso/p121815":"POINT(25.13813 64.4091)", +"http://www.yso.fi/onto/yso/p121816":"POINT(26.90068 60.45759)", +"http://www.yso.fi/onto/yso/p121817":"POINT(26.90444 62.77695)", +"http://www.yso.fi/onto/yso/p121818":"POINT(25.91819 66.32288)", +"http://www.yso.fi/onto/yso/p121823":"POINT(24.24378 63.0396)", +"http://www.yso.fi/onto/yso/p121902":"POINT(23.61919 61.49297)", +"http://www.yso.fi/onto/yso/p121916":"POINT(24.70454 60.8635)", +"http://www.yso.fi/onto/yso/p121936":"POINT(27.60415 65.62569)", +"http://www.yso.fi/onto/yso/p121952":"POINT(22.30511 62.87766)", +"http://www.yso.fi/onto/yso/p122124":"POINT(22.34506 60.4352)", +"http://www.yso.fi/onto/yso/p122125":"POINT(27.64173 66.64702)", +"http://www.yso.fi/onto/yso/p122126":"POINT(27.35484 66.7572)", +"http://www.yso.fi/onto/yso/p122220":"POINT(24.46891 61.02811)", +"http://www.yso.fi/onto/yso/p122223":"POINT(32.0991 62.2953)", +"http://www.yso.fi/onto/yso/p122224":"POINT(25.58027 61.47976)", +"http://www.yso.fi/onto/yso/p122225":"POINT(25.57816 61.49423)", +"http://www.yso.fi/onto/yso/p122231":"POINT(35.440277777778 30.328888888889)", +"http://www.yso.fi/onto/yso/p122466":"", +"http://www.yso.fi/onto/yso/p122500":"POINT(24.22664 61.6089)", +"http://www.yso.fi/onto/yso/p122538":"POINT(24.718055555556 60.065)", +"http://www.yso.fi/onto/yso/p122539":"POINT(23.37167 61.52017)", +"http://www.yso.fi/onto/yso/p122540":"POINT(23.44449 61.55362)", +"http://www.yso.fi/onto/yso/p122542":"POINT(23.32827 61.57905)", +"http://www.yso.fi/onto/yso/p122543":"POINT(23.35065 61.59844)", +"http://www.yso.fi/onto/yso/p122544":"POINT(23.40462 61.65215)", +"http://www.yso.fi/onto/yso/p122548":"POINT(23.85176 61.27611)", +"http://www.yso.fi/onto/yso/p122552":"POINT(27.41129 62.8488)", +"http://www.yso.fi/onto/yso/p122553":"POINT(27.51893 62.86189)", +"http://www.yso.fi/onto/yso/p122554":"POINT(27.43264 62.89509)", +"http://www.yso.fi/onto/yso/p122555":"POINT(29.92418 62.68771)", +"http://www.yso.fi/onto/yso/p122558":"", +"http://www.yso.fi/onto/yso/p122559":"POINT(28.20045 62.70888)", +"http://www.yso.fi/onto/yso/p122560":"POINT(21.30257 60.56123)", +"http://www.yso.fi/onto/yso/p122751":"POINT(30.00423 63.53716)", +"http://www.yso.fi/onto/yso/p122760":"POINT(23.62889 61.46661)", +"http://www.yso.fi/onto/yso/p122766":"POINT(24.52702 60.22644)", +"http://www.yso.fi/onto/yso/p122767":"POINT(24.52235 60.22812)", +"http://www.yso.fi/onto/yso/p122768":"POINT(28.046666666667 53.528333333333)", +"http://www.yso.fi/onto/yso/p122769":"POINT(27.5414 62.31615)", +"http://www.yso.fi/onto/yso/p122770":"POINT(27.51438 62.35082)", +"http://www.yso.fi/onto/yso/p122771":"POINT(25.92913 62.78015)", +"http://www.yso.fi/onto/yso/p122772":"POINT(25.94417 63.11687)", +"http://www.yso.fi/onto/yso/p122773":"POINT(24.11366 60.31908)", +"http://www.yso.fi/onto/yso/p122774":"POINT(21.39551 62.54385)", +"http://www.yso.fi/onto/yso/p122775":"POINT(21.43815 62.53423)", +"http://www.yso.fi/onto/yso/p122776":"POINT(21.59311 60.98874)", +"http://www.yso.fi/onto/yso/p122777":"POINT(24.91318 60.5295)", +"http://www.yso.fi/onto/yso/p122778":"POINT(24.17987 60.57559)", +"http://www.yso.fi/onto/yso/p122783":"POINT(26.91007 63.36782)", +"http://www.yso.fi/onto/yso/p122784":"POINT(26.92744 63.3757)", +"http://www.yso.fi/onto/yso/p123116":"POINT(22.54299 62.16595)", +"http://www.yso.fi/onto/yso/p123148":"POINT(25.41623 60.43377)", +"http://www.yso.fi/onto/yso/p123243":"POINT(23.86041 63.03239)", +"http://www.yso.fi/onto/yso/p123244":"POINT(26.46015 62.70298)", +"http://www.yso.fi/onto/yso/p123245":"POINT(23.61218 63.50869)", +"http://www.yso.fi/onto/yso/p123248":"POINT(29.01412 62.7016)", +"http://www.yso.fi/onto/yso/p123249":"POINT(28.97898 62.67648)", +"http://www.yso.fi/onto/yso/p123250":"POINT(29.0372 62.67098)", +"http://www.yso.fi/onto/yso/p123251":"POINT(28.86301 62.36532)", +"http://www.yso.fi/onto/yso/p123252":"POINT(28.84797 62.35796)", +"http://www.yso.fi/onto/yso/p123262":"POINT(25.00064 60.13601)", +"http://www.yso.fi/onto/yso/p123284":"POINT(14.3475 40.806111)", +"http://www.yso.fi/onto/yso/p123288":"POINT(27.36666667 64.27)", +"http://www.yso.fi/onto/yso/p123289":"POINT(26.46717 60.65216)", +"http://www.yso.fi/onto/yso/p123290":"POINT(25.067 61.65169)", +"http://www.yso.fi/onto/yso/p123291":"POINT(24.08713 67.35905)", +"http://www.yso.fi/onto/yso/p123292":"POINT(23.9453 67.30915)", +"http://www.yso.fi/onto/yso/p123295":"POINT(26.36771 62.38112)", +"http://www.yso.fi/onto/yso/p123296":"POINT(23.64476 62.44755)", +"http://www.yso.fi/onto/yso/p123297":"POINT(23.63113 62.43427)", +"http://www.yso.fi/onto/yso/p123298":"POINT(21.53709 61.807)", +"http://www.yso.fi/onto/yso/p123299":"POINT(26.9743 60.56181)", +"http://www.yso.fi/onto/yso/p123300":"POINT(26.9743 60.56181)", +"http://www.yso.fi/onto/yso/p123301":"POINT(28.97974 66.24272)", +"http://www.yso.fi/onto/yso/p123306":"POINT(23.89653 61.46345)", +"http://www.yso.fi/onto/yso/p123309":"POINT(27.10814 66.321)", +"http://www.yso.fi/onto/yso/p123310":"POINT(108.00472222 53.30277778)", +"http://www.yso.fi/onto/yso/p123319":"POINT(14.45 62.766666666667)", +"http://www.yso.fi/onto/yso/p123320":"POINT(22.8516 63.82384)", +"http://www.yso.fi/onto/yso/p123331":"POINT(79.833333333333 6.9166666666667)", +"http://www.yso.fi/onto/yso/p123424":"POINT(23.84583 61.46873)", +"http://www.yso.fi/onto/yso/p123425":"POINT(23.86139 61.46735)", +"http://www.yso.fi/onto/yso/p123434":"POINT(25.85599 62.22438)", +"http://www.yso.fi/onto/yso/p123437":"POINT(24.47867 61.71505)", +"http://www.yso.fi/onto/yso/p123443":"POINT(24.91405 60.26675)", +"http://www.yso.fi/onto/yso/p123444":"POINT(24.83536 60.16338)", +"http://www.yso.fi/onto/yso/p123451":"POINT(24.96647 60.21846)", +"http://www.yso.fi/onto/yso/p123452":"POINT(24.86546 60.21099)", +"http://www.yso.fi/onto/yso/p123459":"POINT(23.10585 59.85736)", +"http://www.yso.fi/onto/yso/p123460":"POINT(22.30485 61.80868)", +"http://www.yso.fi/onto/yso/p123461":"POINT(27.77076 61.8747)", +"http://www.yso.fi/onto/yso/p123462":"POINT(27.34334 62.4528)", +"http://www.yso.fi/onto/yso/p123463":"POINT(24.69192 60.71045)", +"http://www.yso.fi/onto/yso/p123464":"POINT(29.79968 61.65618)", +"http://www.yso.fi/onto/yso/p123465":"POINT(25.8016 62.27275)", +"http://www.yso.fi/onto/yso/p123466":"POINT(25.57398 63.055)", +"http://www.yso.fi/onto/yso/p123467":"POINT(24.7812 60.26834)", +"http://www.yso.fi/onto/yso/p123468":"POINT(23.07497 61.62237)", +"http://www.yso.fi/onto/yso/p123470":"POINT(26.34572 60.36873)", +"http://www.yso.fi/onto/yso/p123472":"POINT(24.7959 60.24923)", +"http://www.yso.fi/onto/yso/p123473":"POINT(22.87686 63.74463)", +"http://www.yso.fi/onto/yso/p123515":"POINT(160 57)", +"http://www.yso.fi/onto/yso/p123517":"POINT(25.69764 63.06329)", +"http://www.yso.fi/onto/yso/p123519":"POINT(25.78485 63.04385)", +"http://www.yso.fi/onto/yso/p123521":"POINT(27.12658 60.89739)", +"http://www.yso.fi/onto/yso/p123523":"POINT(24.68501 60.22264)", +"http://www.yso.fi/onto/yso/p123524":"POINT(25.74534 60.46052)", +"http://www.yso.fi/onto/yso/p123530":"POINT(22.21124 60.36546)", +"http://www.yso.fi/onto/yso/p123532":"POINT(27.25285 61.69877)", +"http://www.yso.fi/onto/yso/p123533":"POINT(27.76353 62.93581)", +"http://www.yso.fi/onto/yso/p123534":"POINT(22.84329 62.91015)", +"http://www.yso.fi/onto/yso/p123535":"POINT(22.83792 62.93795)", +"http://www.yso.fi/onto/yso/p123536":"POINT(22.87482 62.92476)", +"http://www.yso.fi/onto/yso/p123537":"POINT(21.01047 60.72318)", +"http://www.yso.fi/onto/yso/p123538":"POINT(21.12675 60.45434)", +"http://www.yso.fi/onto/yso/p123884":"POINT(23.66652 60.0621)", +"http://www.yso.fi/onto/yso/p123885":"POINT(25.33064 62.27378)", +"http://www.yso.fi/onto/yso/p123886":"POINT(24.8502 60.16407)", +"http://www.yso.fi/onto/yso/p123887":"POINT(26.62605 65.33389)", +"http://www.yso.fi/onto/yso/p123889":"POINT(24.24709 60.04917)", +"http://www.yso.fi/onto/yso/p123891":"POINT(21.84716 60.68794)", +"http://www.yso.fi/onto/yso/p123892":"POINT(21.81422 60.40294)", +"http://www.yso.fi/onto/yso/p123895":"POINT(30.06252 64.64191)", +"http://www.yso.fi/onto/yso/p123897":"POINT(24.47354 64.67568)", +"http://www.yso.fi/onto/yso/p123899":"POINT(27.67414 61.75954)", +"http://www.yso.fi/onto/yso/p123900":"POINT(22.38793 60.38376)", +"http://www.yso.fi/onto/yso/p123906":"POINT(30.11627194 60.983767)", +"http://www.yso.fi/onto/yso/p123908":"POINT(23.34482 68.11121)", +"http://www.yso.fi/onto/yso/p123951":"POINT(20.25889 60.1174)", +"http://www.yso.fi/onto/yso/p123953":"POINT(28.05737 61.76925)", +"http://www.yso.fi/onto/yso/p123954":"POINT(23.92051 61.49011)", +"http://www.yso.fi/onto/yso/p123955":"POINT(29.59493 62.4086)", +"http://www.yso.fi/onto/yso/p123956":"POINT(23.97746 60.36694)", +"http://www.yso.fi/onto/yso/p123962":"POINT(4.8413888888889 45.758888888889)", +"http://www.yso.fi/onto/yso/p123963":"", +"http://www.yso.fi/onto/yso/p123964":"POINT(26.88368 60.48883)", +"http://www.yso.fi/onto/yso/p123967":"POINT(28.42496 68.4548)", +"http://www.yso.fi/onto/yso/p123968":"POINT(24.41683 61.54865)", +"http://www.yso.fi/onto/yso/p123973":"POINT(26.21503 60.46873)", +"http://www.yso.fi/onto/yso/p123976":"POINT(24.31217 61.04473)", +"http://www.yso.fi/onto/yso/p123996":"POINT(-44.52861111 -22.44277778)", +"http://www.yso.fi/onto/yso/p123999":"POINT(27.29708 61.70325)", +"http://www.yso.fi/onto/yso/p124001":"POINT(23.78056 60.31111)", +"http://www.yso.fi/onto/yso/p124002":"POINT(23.80521 60.29648)", +"http://www.yso.fi/onto/yso/p124003":"POINT(23.79287 60.29315)", +"http://www.yso.fi/onto/yso/p124004":"POINT(24.81885 60.1739)", +"http://www.yso.fi/onto/yso/p124005":"POINT(22.08812 61.24395)", +"http://www.yso.fi/onto/yso/p124006":"POINT(29.47067 66.19076)", +"http://www.yso.fi/onto/yso/p124007":"POINT(29.45736 66.18944)", +"http://www.yso.fi/onto/yso/p124008":"POINT(21.72985 63.07043)", +"http://www.yso.fi/onto/yso/p124009":"POINT(25.38342 65.46324)", +"http://www.yso.fi/onto/yso/p124011":"POINT(30.80401 62.2764)", +"http://www.yso.fi/onto/yso/p124012":"POINT(17 63)", +"http://www.yso.fi/onto/yso/p124013":"POINT(23.83834 61.4979)", +"http://www.yso.fi/onto/yso/p124014":"POINT(23.83812 61.50429)", +"http://www.yso.fi/onto/yso/p124015":"POINT(22.45231 63.39805)", +"http://www.yso.fi/onto/yso/p124016":"POINT(22.45231 63.39805)", +"http://www.yso.fi/onto/yso/p124017":"POINT(25.86967 62.24786)", +"http://www.yso.fi/onto/yso/p124021":"POINT(26.14578 61.07839)", +"http://www.yso.fi/onto/yso/p124142":"POINT(28.501388888889 60.690277777778)", +"http://www.yso.fi/onto/yso/p124369":"POINT(22.701111111111 59.760555555556)", +"http://www.yso.fi/onto/yso/p124371":"POINT(22.18283 59.92672)", +"http://www.yso.fi/onto/yso/p124372":"POINT(22.27587 60.0294)", +"http://www.yso.fi/onto/yso/p124373":"POINT(22.08625 60.08714)", +"http://www.yso.fi/onto/yso/p124374":"POINT(22.19288 59.87253)", +"http://www.yso.fi/onto/yso/p124375":"POINT(21.39424 60.25833)", +"http://www.yso.fi/onto/yso/p124376":"POINT(21.76088 59.95397)", +"http://www.yso.fi/onto/yso/p124377":"POINT(21.49219 60.04143)", +"http://www.yso.fi/onto/yso/p124378":"POINT(21.37474 59.77895)", +"http://www.yso.fi/onto/yso/p124379":"", +"http://www.yso.fi/onto/yso/p124380":"POINT(29.776388888889 60.381944444444)", +"http://www.yso.fi/onto/yso/p124381":"POINT(25.89806 63.99071)", +"http://www.yso.fi/onto/yso/p124384":"POINT(27.73833 64.23114)", +"http://www.yso.fi/onto/yso/p124385":"POINT(26.47179 65.0083)", +"http://www.yso.fi/onto/yso/p124440":"POINT(23.17353 61.67265)", +"http://www.yso.fi/onto/yso/p124450":"POINT(29.79311 62.59328)", +"http://www.yso.fi/onto/yso/p124451":"POINT(29.75929 62.58832)", +"http://www.yso.fi/onto/yso/p124484":"POINT(1.8980555555556 42.788055555556)", +"http://www.yso.fi/onto/yso/p124540":"POINT(14 50)", +"http://www.yso.fi/onto/yso/p124541":"POINT(17 49.5)", +"http://www.yso.fi/onto/yso/p124547":"POINT(29.13882 63.47788)", +"http://www.yso.fi/onto/yso/p124677":"POINT(29.403333333333 60.623611111111)", +"http://www.yso.fi/onto/yso/p124782":"POINT(28.01213 61.39865)", +"http://www.yso.fi/onto/yso/p124783":"POINT(23.61966 63.2119)", +"http://www.yso.fi/onto/yso/p124784":"", +"http://www.yso.fi/onto/yso/p124785":"POINT(21.69863 63.1166)", +"http://www.yso.fi/onto/yso/p124786":"POINT(23.58221 60.71452)", +"http://www.yso.fi/onto/yso/p124844":"POINT(28.18741667 61.05141667)", +"http://www.yso.fi/onto/yso/p124849":"POINT(27.31043 60.7632)", +"http://www.yso.fi/onto/yso/p124850":"POINT(24.35993 60.02905)", +"http://www.yso.fi/onto/yso/p124851":"POINT(27.27381 61.67707)", +"http://www.yso.fi/onto/yso/p124852":"POINT(29.6244 63.48286)", +"http://www.yso.fi/onto/yso/p124853":"POINT(24.74693 60.23125)", +"http://www.yso.fi/onto/yso/p124854":"POINT(29.0038 62.36821)", +"http://www.yso.fi/onto/yso/p124855":"POINT(24.54761 60.52059)", +"http://www.yso.fi/onto/yso/p124856":"POINT(29.00425 66.27313)", +"http://www.yso.fi/onto/yso/p124857":"POINT(27.2639 61.56102)", +"http://www.yso.fi/onto/yso/p124860":"POINT(28.58722 66.12835)", +"http://www.yso.fi/onto/yso/p124894":"POINT(21.33976 62.45064)", +"http://www.yso.fi/onto/yso/p124910":"POINT(23.78779 61.47361)", +"http://www.yso.fi/onto/yso/p124920":"POINT(27.319 61.49145)", +"http://www.yso.fi/onto/yso/p124921":"POINT(27.10565 64.10877)", +"http://www.yso.fi/onto/yso/p124922":"POINT(25.038194444444 60.894305555556)", +"http://www.yso.fi/onto/yso/p124923":"POINT(26.87789 60.44348)", +"http://www.yso.fi/onto/yso/p124924":"POINT(28.28582 62.69639)", +"http://www.yso.fi/onto/yso/p124925":"POINT(27.88694 60.66003)", +"http://www.yso.fi/onto/yso/p124926":"POINT(21.55448 62.37717)", +"http://www.yso.fi/onto/yso/p124927":"POINT(25.0408 64.19451)", +"http://www.yso.fi/onto/yso/p124928":"POINT(28.23772 64.18668)", +"http://www.yso.fi/onto/yso/p124929":"POINT(28.200555555556 59.379166666667)", +"http://www.yso.fi/onto/yso/p124930":"POINT(26.53129 63.63819)", +"http://www.yso.fi/onto/yso/p124931":"POINT(24.93923 60.27442)", +"http://www.yso.fi/onto/yso/p124933":"POINT(23.84859 63.84959)", +"http://www.yso.fi/onto/yso/p124951":"POINT(27.24008 65.22258)", +"http://www.yso.fi/onto/yso/p125104":"POINT(30.17265 62.56359)", +"http://www.yso.fi/onto/yso/p125105":"POINT(23.09991 62.49298)", +"http://www.yso.fi/onto/yso/p125106":"POINT(25.72793 62.24315)", +"http://www.yso.fi/onto/yso/p125107":"POINT(25.73097 62.2478)", +"http://www.yso.fi/onto/yso/p125108":"POINT(22.17514 60.4707)", +"http://www.yso.fi/onto/yso/p125109":"POINT(24.93717 60.20061)", +"http://www.yso.fi/onto/yso/p125113":"POINT(26.24926 60.55376)", +"http://www.yso.fi/onto/yso/p125271":"POINT(17.112777777778 48.144722222222)", +"http://www.yso.fi/onto/yso/p125443":"POINT(22.6143 61.8356)", +"http://www.yso.fi/onto/yso/p125445":"POINT(22.16403 60.44728)", +"http://www.yso.fi/onto/yso/p125452":"POINT(30.10173 62.89327)", +"http://www.yso.fi/onto/yso/p125455":"POINT(-77.5 41)", +"http://www.yso.fi/onto/yso/p125456":"POINT(-75.163333333333 39.953055555556)", +"http://www.yso.fi/onto/yso/p125457":"", +"http://www.yso.fi/onto/yso/p125458":"POINT(3 56)", +"http://www.yso.fi/onto/yso/p125462":"POINT(21.78064 61.50902)", +"http://www.yso.fi/onto/yso/p125463":"POINT(22.41443 62.02255)", +"http://www.yso.fi/onto/yso/p125464":"POINT(22.35147 62.04629)", +"http://www.yso.fi/onto/yso/p125467":"POINT(22.79671 63.5997)", +"http://www.yso.fi/onto/yso/p125468":"POINT(28.90391 62.30061)", +"http://www.yso.fi/onto/yso/p125470":"POINT(22.89137 61.34833)", +"http://www.yso.fi/onto/yso/p125472":"POINT(27.18603 61.67698)", +"http://www.yso.fi/onto/yso/p125474":"POINT(24.22303 65.84879)", +"http://www.yso.fi/onto/yso/p125477":"POINT(24.84258 60.37008)", +"http://www.yso.fi/onto/yso/p125478":"POINT(27.11637 62.98571)", +"http://www.yso.fi/onto/yso/p125479":"POINT(28.38248 62.45047)", +"http://www.yso.fi/onto/yso/p125480":"POINT(23.78541 60.19504)", +"http://www.yso.fi/onto/yso/p125481":"POINT(28.709722222222 60.4625)", +"http://www.yso.fi/onto/yso/p125482":"", +"http://www.yso.fi/onto/yso/p125483":"POINT(22.13468 63.05069)", +"http://www.yso.fi/onto/yso/p125758":"POINT(23.80228 60.58366)", +"http://www.yso.fi/onto/yso/p125759":"POINT(23.80228 60.58366)", +"http://www.yso.fi/onto/yso/p125911":"POINT(-90 70)", +"http://www.yso.fi/onto/yso/p125912":"POINT(27.17729 63.12758)", +"http://www.yso.fi/onto/yso/p125919":"POINT(25.59976 60.53677)", +"http://www.yso.fi/onto/yso/p125920":"POINT(29.7163 62.27188)", +"http://www.yso.fi/onto/yso/p125921":"POINT(23.3187 62.00209)", +"http://www.yso.fi/onto/yso/p125922":"POINT(23.37305 62.06785)", +"http://www.yso.fi/onto/yso/p125924":"POINT(25.77223 60.44911)", +"http://www.yso.fi/onto/yso/p125925":"POINT(23.0391 60.86162)", +"http://www.yso.fi/onto/yso/p125927":"POINT(80 50)", +"http://www.yso.fi/onto/yso/p125929":"POINT(22.06878 60.9176)", +"http://www.yso.fi/onto/yso/p125930":"POINT(23.24496 60.14464)", +"http://www.yso.fi/onto/yso/p125932":"POINT(21.93248 61.60699)", +"http://www.yso.fi/onto/yso/p125933":"POINT(29.53151 62.9575)", +"http://www.yso.fi/onto/yso/p125935":"POINT(24.5314 60.49694)", +"http://www.yso.fi/onto/yso/p125936":"POINT(9.7386111111111 52.374444444444)", +"http://www.yso.fi/onto/yso/p125937":"POINT(26.24833 63.44562)", +"http://www.yso.fi/onto/yso/p125938":"POINT(26.34292 63.35144)", +"http://www.yso.fi/onto/yso/p126209":"POINT(22.48961 61.24116)", +"http://www.yso.fi/onto/yso/p126215":"POINT(23.97579 60.46225)", +"http://www.yso.fi/onto/yso/p126216":"POINT(24.05551 60.52806)", +"http://www.yso.fi/onto/yso/p126218":"POINT(24.38121 60.42491)", +"http://www.yso.fi/onto/yso/p126220":"POINT(24.03801 60.43072)", +"http://www.yso.fi/onto/yso/p126290":"POINT(22.29423 60.46767)", +"http://www.yso.fi/onto/yso/p126429":"", +"http://www.yso.fi/onto/yso/p126432":"POINT(29.38935 62.15076)", +"http://www.yso.fi/onto/yso/p126433":"POINT(29.29146 62.39526)", +"http://www.yso.fi/onto/yso/p126434":"POINT(26.55155 64.66349)", +"http://www.yso.fi/onto/yso/p126435":"POINT(27.59671 62.55598)", +"http://www.yso.fi/onto/yso/p126436":"POINT(24.38175 66.84135)", +"http://www.yso.fi/onto/yso/p126437":"POINT(24.3972 66.83746)", +"http://www.yso.fi/onto/yso/p126438":"POINT(-58.381944444444 -34.599722222222)", +"http://www.yso.fi/onto/yso/p126439":"POINT(28.76865 61.28366)", +"http://www.yso.fi/onto/yso/p126440":"", +"http://www.yso.fi/onto/yso/p126441":"POINT(26.889 62.32223)", +"http://www.yso.fi/onto/yso/p126442":"POINT(27.7119 62.19821)", +"http://www.yso.fi/onto/yso/p126443":"POINT(23.41238 60.84319)", +"http://www.yso.fi/onto/yso/p126444":"", +"http://www.yso.fi/onto/yso/p126452":"POINT(25.485 65.020833333333)", +"http://www.yso.fi/onto/yso/p126454":"POINT(25.59149 60.50167)", +"http://www.yso.fi/onto/yso/p126455":"POINT(25.58406 60.51799)", +"http://www.yso.fi/onto/yso/p126456":"POINT(25.58093 60.54256)", +"http://www.yso.fi/onto/yso/p126457":"POINT(25.62293 60.45541)", +"http://www.yso.fi/onto/yso/p126458":"POINT(25.6122 60.46683)", +"http://www.yso.fi/onto/yso/p126532":"POINT(25.71438 60.53404)", +"http://www.yso.fi/onto/yso/p126533":"POINT(25.70584 60.51888)", +"http://www.yso.fi/onto/yso/p126679":"POINT(29.084561 69.023731)", +"http://www.yso.fi/onto/yso/p126680":"", +"http://www.yso.fi/onto/yso/p126692":"POINT(20.58355 60.09088)", +"http://www.yso.fi/onto/yso/p126693":"POINT(26.63368 60.92401)", +"http://www.yso.fi/onto/yso/p126694":"POINT(5.3233333333333 60.3925)", +"http://www.yso.fi/onto/yso/p126697":"POINT(26.48405 60.5993)", +"http://www.yso.fi/onto/yso/p126698":"POINT(23.75 58.916666666667)", +"http://www.yso.fi/onto/yso/p126700":"POINT(21.51268 60.72179)", +"http://www.yso.fi/onto/yso/p126703":"POINT(21.5411 60.89243)", +"http://www.yso.fi/onto/yso/p126704":"POINT(22.60553 61.43971)", +"http://www.yso.fi/onto/yso/p126705":"POINT(24.38263 67.10197)", +"http://www.yso.fi/onto/yso/p126706":"POINT(24.37593 67.11679)", +"http://www.yso.fi/onto/yso/p127050":"POINT(-83.5 33)", +"http://www.yso.fi/onto/yso/p127164":"", +"http://www.yso.fi/onto/yso/p127193":"POINT(26.32746 64.37501)", +"http://www.yso.fi/onto/yso/p127409":"POINT(22.8399 61.90727)", +"http://www.yso.fi/onto/yso/p127410":"POINT(25.96449 62.41281)", +"http://www.yso.fi/onto/yso/p127411":"POINT(26.77827 60.6587)", +"http://www.yso.fi/onto/yso/p127412":"POINT(26.77827 60.6587)", +"http://www.yso.fi/onto/yso/p127413":"POINT(23.63325 61.79662)", +"http://www.yso.fi/onto/yso/p127415":"POINT(21.75803 60.84515)", +"http://www.yso.fi/onto/yso/p127416":"POINT(21.7587 60.83245)", +"http://www.yso.fi/onto/yso/p127417":"POINT(10.787222222222 52.423055555556)", +"http://www.yso.fi/onto/yso/p127419":"POINT(11.077777777778 49.452777777778)", +"http://www.yso.fi/onto/yso/p127420":"", +"http://www.yso.fi/onto/yso/p127421":"POINT(27.89306 63.16547)", +"http://www.yso.fi/onto/yso/p127422":"POINT(27.26945 61.54509)", +"http://www.yso.fi/onto/yso/p127423":"POINT(24.43476 60.98891)", +"http://www.yso.fi/onto/yso/p127424":"POINT(24.91117 60.17665)", +"http://www.yso.fi/onto/yso/p127425":"POINT(21.74008 61.47015)", +"http://www.yso.fi/onto/yso/p127974":"POINT(14 65)", +"http://www.yso.fi/onto/yso/p127975":"POINT(-119.57 37.73)", +"http://www.yso.fi/onto/yso/p128086":"POINT(22.91904 61.35469)", +"http://www.yso.fi/onto/yso/p128087":"POINT(23.02688 61.89444)", +"http://www.yso.fi/onto/yso/p128088":"POINT(28.05284 64.57551)", +"http://www.yso.fi/onto/yso/p128090":"POINT(23.56112 63.20519)", +"http://www.yso.fi/onto/yso/p128091":"POINT(26.25462 60.6338)", +"http://www.yso.fi/onto/yso/p128092":"POINT(24.01223 68.43672)", +"http://www.yso.fi/onto/yso/p128093":"POINT(23.98798 68.43527)", +"http://www.yso.fi/onto/yso/p128094":"POINT(23.15828 62.61365)", +"http://www.yso.fi/onto/yso/p128095":"POINT(30.03987 64.69171)", +"http://www.yso.fi/onto/yso/p128096":"POINT(21.59537 61.98821)", +"http://www.yso.fi/onto/yso/p128097":"POINT(25.45235 66.86786)", +"http://www.yso.fi/onto/yso/p128098":"POINT(21.35391 60.55014)", +"http://www.yso.fi/onto/yso/p128099":"POINT(24.65307 60.15054)", +"http://www.yso.fi/onto/yso/p128100":"POINT(21.43021 60.22871)", +"http://www.yso.fi/onto/yso/p128101":"POINT(25.16357 60.84884)", +"http://www.yso.fi/onto/yso/p128102":"POINT(25.14881 60.86661)", +"http://www.yso.fi/onto/yso/p128103":"POINT(28.60268 61.91036)", +"http://www.yso.fi/onto/yso/p128104":"POINT(28.60268 61.91036)", +"http://www.yso.fi/onto/yso/p128105":"POINT(26.77338 64.3512)", +"http://www.yso.fi/onto/yso/p128106":"POINT(26.9055 64.33062)", +"http://www.yso.fi/onto/yso/p128107":"", +"http://www.yso.fi/onto/yso/p128107":"", +"http://www.yso.fi/onto/yso/p128108":"POINT(25.3463 61.02061)", +"http://www.yso.fi/onto/yso/p128110":"POINT(25.264 61.01883)", +"http://www.yso.fi/onto/yso/p128111":"POINT(25.27781 61.04219)", +"http://www.yso.fi/onto/yso/p128112":"POINT(25.31635 61.00025)", +"http://www.yso.fi/onto/yso/p128113":"POINT(25.23591 61.00843)", +"http://www.yso.fi/onto/yso/p128114":"POINT(25.26928 61.00015)", +"http://www.yso.fi/onto/yso/p128115":"POINT(25.36054 61.00107)", +"http://www.yso.fi/onto/yso/p128117":"POINT(6.8783777777778 49.37715)", +"http://www.yso.fi/onto/yso/p128120":"POINT(22.59496 62.76159)", +"http://www.yso.fi/onto/yso/p128204":"POINT(-122.433333 37.766667)", +"http://www.yso.fi/onto/yso/p128208":"POINT(24.29998 60.4122)", +"http://www.yso.fi/onto/yso/p128250":"POINT(23.13319 62.56019)", +"http://www.yso.fi/onto/yso/p128613":"POINT(27.077777777778 62.2875)", +"http://www.yso.fi/onto/yso/p128893":"POINT(39.85 57.616666666667)", +"http://www.yso.fi/onto/yso/p128894":"POINT(29.25 57.316666666667)", +"http://www.yso.fi/onto/yso/p128896":"POINT(21.24792 62.70491)", +"http://www.yso.fi/onto/yso/p128897":"POINT(26.45023 64.50619)", +"http://www.yso.fi/onto/yso/p128898":"POINT(27.57869 61.06733)", +"http://www.yso.fi/onto/yso/p128899":"POINT(24.24258 60.98977)", +"http://www.yso.fi/onto/yso/p128900":"POINT(24.35802 60.96041)", +"http://www.yso.fi/onto/yso/p128901":"POINT(24.15144 61.05607)", +"http://www.yso.fi/onto/yso/p128904":"POINT(27.98814 60.67585)", +"http://www.yso.fi/onto/yso/p128907":"POINT(26.04173 63.67173)", +"http://www.yso.fi/onto/yso/p128908":"POINT(21.84762 61.45142)", +"http://www.yso.fi/onto/yso/p128910":"POINT(20.61667 50.73333)", +"http://www.yso.fi/onto/yso/p129133":"POINT(35.883333333333 65.083333333333)", +"http://www.yso.fi/onto/yso/p129134":"POINT(44.483333333333 48.7)", +"http://www.yso.fi/onto/yso/p129135":"POINT(24.6701 60.20355)", +"http://www.yso.fi/onto/yso/p129136":"POINT(25.23437 60.71347)", +"http://www.yso.fi/onto/yso/p129137":"POINT(22.27477 60.47739)", +"http://www.yso.fi/onto/yso/p129138":"POINT(28.886111111111 60.7625)", +"http://www.yso.fi/onto/yso/p129139":"POINT(25.35246 60.9579)", +"http://www.yso.fi/onto/yso/p129140":"POINT(27.92618 66.57667)", +"http://www.yso.fi/onto/yso/p129141":"POINT(28.08382 66.55792)", +"http://www.yso.fi/onto/yso/p129142":"POINT(23.98338 67.15011)", +"http://www.yso.fi/onto/yso/p129143":"POINT(15.3 37.85)", +"http://www.yso.fi/onto/yso/p129146":"POINT(26 42)", +"http://www.yso.fi/onto/yso/p129354":"", +"http://www.yso.fi/onto/yso/p129354":"", +"http://www.yso.fi/onto/yso/p129374":"POINT(29.5483 61.3152)", +"http://www.yso.fi/onto/yso/p129543":"POINT(29.83714 64.38862)", +"http://www.yso.fi/onto/yso/p129629":"POINT(27.2005 62.81092)", +"http://www.yso.fi/onto/yso/p129736":"POINT(24.71623 62.02759)", +"http://www.yso.fi/onto/yso/p129737":"POINT(24.71605 62.02952)", +"http://www.yso.fi/onto/yso/p129738":"POINT(16 78)", +"http://www.yso.fi/onto/yso/p129739":"POINT(25.24303 60.45444)", +"http://www.yso.fi/onto/yso/p129740":"POINT(26.24988 60.58463)", +"http://www.yso.fi/onto/yso/p129741":"", +"http://www.yso.fi/onto/yso/p129742":"POINT(20.82522 63.05849)", +"http://www.yso.fi/onto/yso/p129743":"POINT(23.25801 61.72255)", +"http://www.yso.fi/onto/yso/p129744":"POINT(25.71015 62.26342)", +"http://www.yso.fi/onto/yso/p129745":"POINT(25.73559 62.27654)", +"http://www.yso.fi/onto/yso/p129746":"POINT(23.752943 65.573016)", +"http://www.yso.fi/onto/yso/p129747":"POINT(24.00156 61.28679)", +"http://www.yso.fi/onto/yso/p129748":"POINT(24.01389 61.28561)", +"http://www.yso.fi/onto/yso/p129749":"POINT(24.00126 61.28386)", +"http://www.yso.fi/onto/yso/p129750":"POINT(24.69111 60.68576)", +"http://www.yso.fi/onto/yso/p129751":"POINT(30.07295 61.95199)", +"http://www.yso.fi/onto/yso/p129752":"POINT(24.42377 60.86591)", +"http://www.yso.fi/onto/yso/p129753":"POINT(26.23172 61.50083)", +"http://www.yso.fi/onto/yso/p129754":"", +"http://www.yso.fi/onto/yso/p129754":"", +"http://www.yso.fi/onto/yso/p129979":"POINT(29.14657 66.30445)", +"http://www.yso.fi/onto/yso/p129980":"POINT(29.39128 66.27161)", +"http://www.yso.fi/onto/yso/p130215":"POINT(23.81277 60.02542)", +"http://www.yso.fi/onto/yso/p130386":"POINT(32.601388888889 25.728888888889)", +"http://www.yso.fi/onto/yso/p130575":"", +"http://www.yso.fi/onto/yso/p130585":"POINT(30.25815 62.06807)", +"http://www.yso.fi/onto/yso/p130586":"POINT(24.76201 60.27736)", +"http://www.yso.fi/onto/yso/p130587":"POINT(24.74004 60.28813)", +"http://www.yso.fi/onto/yso/p130588":"POINT(86.916666666667 50.916666666667)", +"http://www.yso.fi/onto/yso/p130589":"", +"http://www.yso.fi/onto/yso/p130589":"POINT(24.90833333 67.65694444)", +"http://www.yso.fi/onto/yso/p130590":"POINT(23.11314 59.90576)", +"http://www.yso.fi/onto/yso/p130591":"POINT(23.94084 61.24212)", +"http://www.yso.fi/onto/yso/p130592":"POINT(23.94084 61.24212)", +"http://www.yso.fi/onto/yso/p130593":"POINT(23.68909 62.24516)", +"http://www.yso.fi/onto/yso/p130594":"POINT(22.71544 62.73019)", +"http://www.yso.fi/onto/yso/p130595":"POINT(19.9166 59.8416)", +"http://www.yso.fi/onto/yso/p130596":"POINT(28.37632 64.4729)", +"http://www.yso.fi/onto/yso/p130597":"POINT(27.98118 63.1743)", +"http://www.yso.fi/onto/yso/p130598":"POINT(1.5291666666667 49.076111111111)", +"http://www.yso.fi/onto/yso/p130599":"POINT(22.77795 60.61821)", +"http://www.yso.fi/onto/yso/p130600":"POINT(12.289983 41.755913)", +"http://www.yso.fi/onto/yso/p130671":"POINT(26.3009 60.22056)", +"http://www.yso.fi/onto/yso/p130850":"POINT(28.40533 67.23632)", +"http://www.yso.fi/onto/yso/p131100":"POINT(-19.7 63.983333333333)", +"http://www.yso.fi/onto/yso/p131101":"POINT(26.23887 61.56488)", +"http://www.yso.fi/onto/yso/p131102":"POINT(23.45479 60.7375)", +"http://www.yso.fi/onto/yso/p131103":"POINT(10.6 36.416666666667)", +"http://www.yso.fi/onto/yso/p131104":"POINT(27.10107 62.52859)", +"http://www.yso.fi/onto/yso/p131105":"POINT(27.0573 62.54254)", +"http://www.yso.fi/onto/yso/p131106":"POINT(27.03492 62.509)", +"http://www.yso.fi/onto/yso/p131108":"POINT(20.09719 59.98435)", +"http://www.yso.fi/onto/yso/p131109":"POINT(28.91555 61.22108)", +"http://www.yso.fi/onto/yso/p131111":"POINT(22.32999 60.54561)", +"http://www.yso.fi/onto/yso/p131112":"POINT(26.72634 65.19732)", +"http://www.yso.fi/onto/yso/p131113":"POINT(25.10769 60.26997)", +"http://www.yso.fi/onto/yso/p131226":"POINT(26.95698 60.49899)", +"http://www.yso.fi/onto/yso/p131237":"POINT(15.316666666667 -4.3166666666667)", +"http://www.yso.fi/onto/yso/p131238":"POINT(15.383330555556 -0.75)", +"http://www.yso.fi/onto/yso/p131239":"POINT(-0.57 52.22)", +"http://www.yso.fi/onto/yso/p131449":"POINT(22.98651 60.27663)", +"http://www.yso.fi/onto/yso/p131451":"POINT(23.29467 62.65533)", +"http://www.yso.fi/onto/yso/p131454":"POINT(23.37305 62.3788)", +"http://www.yso.fi/onto/yso/p131457":"POINT(23.33126 62.39034)", +"http://www.yso.fi/onto/yso/p131460":"POINT(23.83407 62.64999)", +"http://www.yso.fi/onto/yso/p131461":"POINT(23.79197 62.61165)", +"http://www.yso.fi/onto/yso/p131463":"POINT(23.87119 62.60028)", +"http://www.yso.fi/onto/yso/p131464":"POINT(23.89229 62.60605)", +"http://www.yso.fi/onto/yso/p131465":"POINT(26.76248 60.76405)", +"http://www.yso.fi/onto/yso/p131466":"POINT(25.3374 61.68502)", +"http://www.yso.fi/onto/yso/p131467":"POINT(24.02694 60.41566)", +"http://www.yso.fi/onto/yso/p131468":"POINT(22.25666 61.07582)", +"http://www.yso.fi/onto/yso/p131470":"POINT(24.30047 61.94146)", +"http://www.yso.fi/onto/yso/p131471":"POINT(24.33876 61.93189)", +"http://www.yso.fi/onto/yso/p131472":"POINT(24.13817 61.90286)", +"http://www.yso.fi/onto/yso/p131473":"POINT(24.02065 62.29311)", +"http://www.yso.fi/onto/yso/p131474":"POINT(27.231 61.69763)", +"http://www.yso.fi/onto/yso/p131475":"POINT(23.436111111111 59.975)", +"http://www.yso.fi/onto/yso/p131481":"POINT(-106 34)", +"http://www.yso.fi/onto/yso/p131485":"POINT(25.56851 62.27053)", +"http://www.yso.fi/onto/yso/p131486":"POINT(23.30758 63.40533)", +"http://www.yso.fi/onto/yso/p131487":"POINT(22.69223 62.10526)", +"http://www.yso.fi/onto/yso/p131488":"POINT(22.63508 62.10347)", +"http://www.yso.fi/onto/yso/p131592":"POINT(24.25179 61.95256)", +"http://www.yso.fi/onto/yso/p131687":"POINT(22.34902 60.71964)", +"http://www.yso.fi/onto/yso/p131836":"POINT(23.2669 61.03668)", +"http://www.yso.fi/onto/yso/p132000":"POINT(20.95 64.75)", +"http://www.yso.fi/onto/yso/p132001":"POINT(26.20616 61.08079)", +"http://www.yso.fi/onto/yso/p132002":"POINT(26.19094 60.33612)", +"http://www.yso.fi/onto/yso/p132004":"POINT(21.85481 61.7913)", +"http://www.yso.fi/onto/yso/p132005":"POINT(21.89413 61.78755)", +"http://www.yso.fi/onto/yso/p132007":"POINT(22.84516 62.35898)", +"http://www.yso.fi/onto/yso/p132008":"", +"http://www.yso.fi/onto/yso/p132010":"POINT(22.11317 60.45442)", +"http://www.yso.fi/onto/yso/p132012":"POINT(23.56395 61.32995)", +"http://www.yso.fi/onto/yso/p132013":"POINT(23.77366 61.3869)", +"http://www.yso.fi/onto/yso/p132514":"POINT(22.29915 61.06067)", +"http://www.yso.fi/onto/yso/p132516":"POINT(13.5 58.916666666667)", +"http://www.yso.fi/onto/yso/p132518":"POINT(9.3166666666667 47.65)", +"http://www.yso.fi/onto/yso/p132519":"POINT(27.491666666667 58.676666666667)", +"http://www.yso.fi/onto/yso/p132522":"POINT(-84 45.75)", +"http://www.yso.fi/onto/yso/p132524":"POINT(28.04008 61.04051)", +"http://www.yso.fi/onto/yso/p132525":"POINT(28.04008 61.04051)", +"http://www.yso.fi/onto/yso/p132526":"POINT(27.84117 60.58225)", +"http://www.yso.fi/onto/yso/p132531":"POINT(23.33167 63.4184)", +"http://www.yso.fi/onto/yso/p132532":"POINT(23.30527 63.42406)", +"http://www.yso.fi/onto/yso/p132534":"POINT(24.51896 66.70142)", +"http://www.yso.fi/onto/yso/p132535":"POINT(24.49139 66.70065)", +"http://www.yso.fi/onto/yso/p132576":"POINT(31.28961 62.63743)", +"http://www.yso.fi/onto/yso/p132671":"POINT(22.02951 62.46314)", +"http://www.yso.fi/onto/yso/p132678":"POINT(21.97304 60.99678)", +"http://www.yso.fi/onto/yso/p132712":"POINT(102 16)", +"http://www.yso.fi/onto/yso/p132731":"POINT(30.10595 64.65921)", +"http://www.yso.fi/onto/yso/p132769":"POINT(26.17747 60.55229)", +"http://www.yso.fi/onto/yso/p133044":"POINT(28.06402 62.68871)", +"http://www.yso.fi/onto/yso/p133109":"POINT(22.32015 61.84475)", +"http://www.yso.fi/onto/yso/p133151":"POINT(22.43313 61.00944)", +"http://www.yso.fi/onto/yso/p133152":"POINT(27.98915 65.91883)", +"http://www.yso.fi/onto/yso/p133153":"POINT(27.95216 65.91967)", +"http://www.yso.fi/onto/yso/p133154":"POINT(25.38863 61.97269)", +"http://www.yso.fi/onto/yso/p133155":"POINT(21.19385 60.86113)", +"http://www.yso.fi/onto/yso/p133156":"POINT(25.28093 64.54672)", +"http://www.yso.fi/onto/yso/p133157":"POINT(24.28814 63.97837)", +"http://www.yso.fi/onto/yso/p133160":"POINT(25.56272 60.96651)", +"http://www.yso.fi/onto/yso/p133162":"POINT(24.15547 61.18726)", +"http://www.yso.fi/onto/yso/p133163":"POINT(24.00835 61.15509)", +"http://www.yso.fi/onto/yso/p133166":"POINT(23.0416 61.39908)", +"http://www.yso.fi/onto/yso/p133695":"POINT(23.71117 61.49752)", +"http://www.yso.fi/onto/yso/p133698":"POINT(23.60716 61.50604)", +"http://www.yso.fi/onto/yso/p133701":"POINT(23.03541 61.8315)", +"http://www.yso.fi/onto/yso/p133705":"POINT(21.70395 62.02539)", +"http://www.yso.fi/onto/yso/p133707":"POINT(25.53908 64.06328)", +"http://www.yso.fi/onto/yso/p133708":"POINT(25.54542 64.03283)", +"http://www.yso.fi/onto/yso/p133710":"POINT(25.53062 64.00725)", +"http://www.yso.fi/onto/yso/p133711":"POINT(25.56302 64.08493)", +"http://www.yso.fi/onto/yso/p133713":"POINT(25.62717 64.02209)", +"http://www.yso.fi/onto/yso/p133715":"POINT(25.7292 67.18732)", +"http://www.yso.fi/onto/yso/p133719":"POINT(23.8601 61.83298)", +"http://www.yso.fi/onto/yso/p133721":"POINT(23.17406 63.86882)", +"http://www.yso.fi/onto/yso/p133722":"POINT(23.18494 63.8448)", +"http://www.yso.fi/onto/yso/p133725":"POINT(27.38908 62.19279)", +"http://www.yso.fi/onto/yso/p134208":"", +"http://www.yso.fi/onto/yso/p134209":"", +"http://www.yso.fi/onto/yso/p134236":"", +"http://www.yso.fi/onto/yso/p134367":"POINT(-135.33166666667 57.052777777778)", +"http://www.yso.fi/onto/yso/p134574":"POINT(27.2916 62.2101)", +"http://www.yso.fi/onto/yso/p134575":"POINT(21.56192 62.35868)", +"http://www.yso.fi/onto/yso/p134576":"POINT(24.77612 60.16208)", +"http://www.yso.fi/onto/yso/p134579":"", +"http://www.yso.fi/onto/yso/p134580":"POINT(29.52161 62.34108)", +"http://www.yso.fi/onto/yso/p134581":"POINT(22.38304 60.3878)", +"http://www.yso.fi/onto/yso/p134582":"POINT(25.02162 60.27519)", +"http://www.yso.fi/onto/yso/p134583":"POINT(29.532 62.86493)", +"http://www.yso.fi/onto/yso/p134586":"POINT(-120.575 43.935833)", +"http://www.yso.fi/onto/yso/p134588":"POINT(25.60984 64.18949)", +"http://www.yso.fi/onto/yso/p134590":"POINT(25.67868 64.17031)", +"http://www.yso.fi/onto/yso/p134591":"POINT(25.53053 64.16758)", +"http://www.yso.fi/onto/yso/p134592":"POINT(25.47159 64.18054)", +"http://www.yso.fi/onto/yso/p134593":"POINT(25.41465 64.1588)", +"http://www.yso.fi/onto/yso/p134597":"POINT(23.59319 63.96918)", +"http://www.yso.fi/onto/yso/p134601":"POINT(22.19952 60.42712)", +"http://www.yso.fi/onto/yso/p134741":"POINT(50.933333333333 55.55)", +"http://www.yso.fi/onto/yso/p134742":"POINT(56.266666666667 54.466666666667)", +"http://www.yso.fi/onto/yso/p134743":"POINT(47.096111 55.459444)", +"http://www.yso.fi/onto/yso/p134967":"POINT(26.78453 64.44455)", +"http://www.yso.fi/onto/yso/p135245":"POINT(139.63333333333 35.433333333333)", +"http://www.yso.fi/onto/yso/p135357":"POINT(23.26125 61.41705)", +"http://www.yso.fi/onto/yso/p135358":"POINT(23.26125 61.41705)", +"http://www.yso.fi/onto/yso/p135363":"POINT(35.306389 31.698889)", +"http://www.yso.fi/onto/yso/p135364":"POINT(24.65052 60.38868)", +"http://www.yso.fi/onto/yso/p135369":"POINT(22.55142 61.01252)", +"http://www.yso.fi/onto/yso/p135372":"POINT(27.48532 61.61568)", +"http://www.yso.fi/onto/yso/p135373":"POINT(24.78378 60.2167)", +"http://www.yso.fi/onto/yso/p135374":"POINT(22.383333333333 60.066666666667)", +"http://www.yso.fi/onto/yso/p135375":"POINT(25.10619 60.20359)", +"http://www.yso.fi/onto/yso/p135376":"POINT(22.87463 61.65674)", +"http://www.yso.fi/onto/yso/p135466":"POINT(23.09319 63.59476)", +"http://www.yso.fi/onto/yso/p135467":"POINT(23.11983 63.62384)", +"http://www.yso.fi/onto/yso/p135499":"POINT(21.72186 62.73922)", +"http://www.yso.fi/onto/yso/p135503":"POINT(21.42581 62.75543)", +"http://www.yso.fi/onto/yso/p135826":"POINT(23.58578 67.15668)", +"http://www.yso.fi/onto/yso/p135827":"POINT(23.72824 67.03747)", +"http://www.yso.fi/onto/yso/p135828":"POINT(23.3378 63.77084)", +"http://www.yso.fi/onto/yso/p135831":"POINT(26.7635 64.56956)", +"http://www.yso.fi/onto/yso/p135832":"POINT(24.12584 61.45365)", +"http://www.yso.fi/onto/yso/p135834":"POINT(27.30318 62.16118)", +"http://www.yso.fi/onto/yso/p135835":"POINT(27.20231 62.157)", +"http://www.yso.fi/onto/yso/p135838":"POINT(27.33524 63.41303)", +"http://www.yso.fi/onto/yso/p135839":"POINT(21.69351 63.01549)", +"http://www.yso.fi/onto/yso/p135842":"POINT(23.15731 60.39933)", +"http://www.yso.fi/onto/yso/p136710":"POINT(-61.583333333333 16.25)", +"http://www.yso.fi/onto/yso/p136713":"POINT(25.11715 60.24319)", +"http://www.yso.fi/onto/yso/p136714":"POINT(25.09886 60.25298)", +"http://www.yso.fi/onto/yso/p136718":"POINT(23.31397 62.19422)", +"http://www.yso.fi/onto/yso/p136719":"POINT(23.8789 61.2321)", +"http://www.yso.fi/onto/yso/p136720":"POINT(30.15 60.63333333)", +"http://www.yso.fi/onto/yso/p136721":"POINT(30.53247 63.30216)", +"http://www.yso.fi/onto/yso/p136722":"POINT(21.71828 60.68249)", +"http://www.yso.fi/onto/yso/p136723":"POINT(25.64111 60.9346)", +"http://www.yso.fi/onto/yso/p136724":"POINT(25.70252 64.99074)", +"http://www.yso.fi/onto/yso/p137148":"POINT(-113.5 28)", +"http://www.yso.fi/onto/yso/p137274":"POINT(24.48422 60.85391)", +"http://www.yso.fi/onto/yso/p137276":"POINT(28.46983 64.13456)", +"http://www.yso.fi/onto/yso/p137277":"POINT(28.53222 64.09933)", +"http://www.yso.fi/onto/yso/p137279":"", +"http://www.yso.fi/onto/yso/p137279":"", +"http://www.yso.fi/onto/yso/p137281":"POINT(25.33358 60.36104)", +"http://www.yso.fi/onto/yso/p137282":"POINT(25.3303 60.36866)", +"http://www.yso.fi/onto/yso/p137285":"POINT(20.0606 60.43273)", +"http://www.yso.fi/onto/yso/p137287":"", +"http://www.yso.fi/onto/yso/p137288":"", +"http://www.yso.fi/onto/yso/p137289":"POINT(29.869166666667 60.343333333333)", +"http://www.yso.fi/onto/yso/p137290":"POINT(23.04436 63.8531)", +"http://www.yso.fi/onto/yso/p137291":"POINT(26.65767 60.89135)", +"http://www.yso.fi/onto/yso/p137292":"POINT(29.5509 62.67464)", +"http://www.yso.fi/onto/yso/p137293":"POINT(24.89095 60.25608)", +"http://www.yso.fi/onto/yso/p137294":"", +"http://www.yso.fi/onto/yso/p137295":"POINT(23.7497 63.50182)", +"http://www.yso.fi/onto/yso/p137296":"POINT(24.93905 63.89139)", +"http://www.yso.fi/onto/yso/p137297":"POINT(23.02924 62.98347)", +"http://www.yso.fi/onto/yso/p137479":"POINT(22.54403 59.97615)", +"http://www.yso.fi/onto/yso/p137718":"POINT(34.839167 38.670556)", +"http://www.yso.fi/onto/yso/p137744":"POINT(24.4975 61.3375)", +"http://www.yso.fi/onto/yso/p138238":"POINT(26.0003 66.74739)", +"http://www.yso.fi/onto/yso/p138239":"POINT(11.051944444444 50.861111111111)", +"http://www.yso.fi/onto/yso/p138240":"POINT(23.64187 61.50378)", +"http://www.yso.fi/onto/yso/p138251":"POINT(23.49481 62.96958)", +"http://www.yso.fi/onto/yso/p138252":"POINT(23.26817 62.67933)", +"http://www.yso.fi/onto/yso/p138254":"POINT(24.21959 62.5457)", +"http://www.yso.fi/onto/yso/p138257":"POINT(24.31831 62.59622)", +"http://www.yso.fi/onto/yso/p138259":"POINT(24.26484 62.58854)", +"http://www.yso.fi/onto/yso/p138266":"POINT(28.10057 61.0637)", +"http://www.yso.fi/onto/yso/p138272":"POINT(28.81439 62.23388)", +"http://www.yso.fi/onto/yso/p138273":"POINT(22.37819 61.04144)", +"http://www.yso.fi/onto/yso/p138274":"POINT(23.52601 62.94461)", +"http://www.yso.fi/onto/yso/p138275":"POINT(23.51979 62.95681)", +"http://www.yso.fi/onto/yso/p138276":"POINT(23.80828 61.43474)", +"http://www.yso.fi/onto/yso/p138278":"POINT(21.91102 63.22734)", +"http://www.yso.fi/onto/yso/p138279":"POINT(25.59015 60.64133)", +"http://www.yso.fi/onto/yso/p138280":"POINT(21.57833 61.13391)", +"http://www.yso.fi/onto/yso/p138281":"POINT(26.3366 61.03362)", +"http://www.yso.fi/onto/yso/p138282":"POINT(22.77669 63.00203)", +"http://www.yso.fi/onto/yso/p138291":"POINT(-1.9169444444444 42.881944444444)", +"http://www.yso.fi/onto/yso/p138411":"POINT(25.43303 62.72674)", +"http://www.yso.fi/onto/yso/p138444":"POINT(24.87019 60.24842)", +"http://www.yso.fi/onto/yso/p138514":"POINT(23.68713 66.45599)", +"http://www.yso.fi/onto/yso/p138634":"", +"http://www.yso.fi/onto/yso/p138634":"POINT(23.766742 61.50052)", +"http://www.yso.fi/onto/yso/p138635":"POINT(29.42053 63.03851)", +"http://www.yso.fi/onto/yso/p138636":"POINT(22.42903 63.32059)", +"http://www.yso.fi/onto/yso/p138638":"POINT(23.70262 66.37947)", +"http://www.yso.fi/onto/yso/p138639":"POINT(23.783 66.38851)", +"http://www.yso.fi/onto/yso/p138640":"POINT(27.34608 64.74743)", +"http://www.yso.fi/onto/yso/p138641":"POINT(28.68744 62.79579)", +"http://www.yso.fi/onto/yso/p138642":"", +"http://www.yso.fi/onto/yso/p138643":"", +"http://www.yso.fi/onto/yso/p138644":"POINT(28.16351 62.91501)", +"http://www.yso.fi/onto/yso/p138645":"POINT(26.18966 61.42572)", +"http://www.yso.fi/onto/yso/p138646":"POINT(26.11895 61.46176)", +"http://www.yso.fi/onto/yso/p138647":"POINT(26.21528 61.47072)", +"http://www.yso.fi/onto/yso/p138648":"POINT(27.3459 66.36414)", +"http://www.yso.fi/onto/yso/p138649":"POINT(22.24243 63.21761)", +"http://www.yso.fi/onto/yso/p138653":"POINT(-0.187 5.6037)", +"http://www.yso.fi/onto/yso/p138838":"", +"http://www.yso.fi/onto/yso/p138842":"POINT(23.69302 63.01684)", +"http://www.yso.fi/onto/yso/p138843":"POINT(24.3667 61.05)", +"http://www.yso.fi/onto/yso/p138844":"POINT(24.02364 61.00502)", +"http://www.yso.fi/onto/yso/p138849":"POINT(23.96144 60.18109)", +"http://www.yso.fi/onto/yso/p138850":"POINT(29.46853 61.6068)", +"http://www.yso.fi/onto/yso/p138852":"", +"http://www.yso.fi/onto/yso/p138852":"POINT(24.54444444 65.80138889)", +"http://www.yso.fi/onto/yso/p138854":"POINT(24.15547 61.18726)", +"http://www.yso.fi/onto/yso/p138880":"POINT(24.62844 65.71438)", +"http://www.yso.fi/onto/yso/p139229":"POINT(24.31583 67.52538)", +"http://www.yso.fi/onto/yso/p139232":"POINT(22.98002 62.17947)", +"http://www.yso.fi/onto/yso/p139366":"POINT(21.63581 61.97832)", +"http://www.yso.fi/onto/yso/p139368":"POINT(23.56798 60.21675)", +"http://www.yso.fi/onto/yso/p139376":"POINT(24.15746 67.90324)", +"http://www.yso.fi/onto/yso/p139378":"POINT(26.79842 64.52215)", +"http://www.yso.fi/onto/yso/p139381":"POINT(24.10045 66.90781)", +"http://www.yso.fi/onto/yso/p139406":"POINT(26.82972 60.74714)", +"http://www.yso.fi/onto/yso/p139422":"POINT(25.10025 60.32498)", +"http://www.yso.fi/onto/yso/p139444":"POINT(27.42135 64.4439)", +"http://www.yso.fi/onto/yso/p139447":"POINT(25.35757 63.50227)", +"http://www.yso.fi/onto/yso/p139449":"POINT(28.08978 65.12425)", +"http://www.yso.fi/onto/yso/p139481":"POINT(29.0656 63.63389)", +"http://www.yso.fi/onto/yso/p139482":"POINT(28.90029 63.61602)", +"http://www.yso.fi/onto/yso/p139483":"POINT(28.86335 63.70342)", +"http://www.yso.fi/onto/yso/p139484":"POINT(28.58948 63.77882)", +"http://www.yso.fi/onto/yso/p139486":"POINT(24.427 62.47294)", +"http://www.yso.fi/onto/yso/p139489":"POINT(24.50344 62.4497)", +"http://www.yso.fi/onto/yso/p139490":"POINT(27.15219 63.05366)", +"http://www.yso.fi/onto/yso/p139491":"POINT(23.93867 60.69789)", +"http://www.yso.fi/onto/yso/p139492":"POINT(24.51847 60.98994)", +"http://www.yso.fi/onto/yso/p139494":"POINT(24.51796 60.98447)", +"http://www.yso.fi/onto/yso/p139495":"POINT(24.58468 60.84242)", +"http://www.yso.fi/onto/yso/p139501":"POINT(21.80936 61.57171)", +"http://www.yso.fi/onto/yso/p139504":"POINT(23.58405 60.64317)", +"http://www.yso.fi/onto/yso/p139505":"POINT(23.58992 60.61415)", +"http://www.yso.fi/onto/yso/p139508":"POINT(26.24024 60.67193)", +"http://www.yso.fi/onto/yso/p139509":"POINT(29.68221 62.62395)", +"http://www.yso.fi/onto/yso/p139510":"POINT(29.71824 62.61723)", +"http://www.yso.fi/onto/yso/p139511":"POINT(25.4829 64.11178)", +"http://www.yso.fi/onto/yso/p139512":"POINT(25.02449 64.38256)", +"http://www.yso.fi/onto/yso/p139514":"POINT(26.37602 61.46229)", +"http://www.yso.fi/onto/yso/p139515":"POINT(25.76321 61.57604)", +"http://www.yso.fi/onto/yso/p139516":"POINT(25.80656 61.58583)", +"http://www.yso.fi/onto/yso/p139530":"POINT(22.84401 62.76349)", +"http://www.yso.fi/onto/yso/p139541":"POINT(27.42577 66.37266)", +"http://www.yso.fi/onto/yso/p139542":"POINT(24.74333 64.83728)", +"http://www.yso.fi/onto/yso/p139543":"POINT(23.47049 60.84952)", +"http://www.yso.fi/onto/yso/p139544":"POINT(25.0037 60.64998)", +"http://www.yso.fi/onto/yso/p139545":"POINT(25.98067 66.75222)", +"http://www.yso.fi/onto/yso/p139546":"POINT(24.95191 60.42403)", +"http://www.yso.fi/onto/yso/p139590":"", +"http://www.yso.fi/onto/yso/p139596":"POINT(28.96859 65.28639)", +"http://www.yso.fi/onto/yso/p139597":"POINT(25.85296 61.50682)", +"http://www.yso.fi/onto/yso/p139598":"POINT(24.92592 60.39831)", +"http://www.yso.fi/onto/yso/p139624":"POINT(25.60375 60.89645)", +"http://www.yso.fi/onto/yso/p139625":"POINT(23.63496 63.19523)", +"http://www.yso.fi/onto/yso/p139761":"POINT(23.91069 61.47058)", +"http://www.yso.fi/onto/yso/p139765":"", +"http://www.yso.fi/onto/yso/p139766":"POINT(23.20422 61.30307)", +"http://www.yso.fi/onto/yso/p139767":"POINT(23.45403 61.03412)", +"http://www.yso.fi/onto/yso/p139802":"POINT(23.7117 60.82539)", +"http://www.yso.fi/onto/yso/p139803":"POINT(26.91966 61.01635)", +"http://www.yso.fi/onto/yso/p139804":"POINT(26.72104 61.17446)", +"http://www.yso.fi/onto/yso/p139805":"POINT(24.63834 60.69424)", +"http://www.yso.fi/onto/yso/p139855":"POINT(22.19969 62.4078)", +"http://www.yso.fi/onto/yso/p139876":"POINT(24.87874 60.24199)", +"http://www.yso.fi/onto/yso/p139881":"POINT(17.0396 51.1035)", +"http://www.yso.fi/onto/yso/p139882":"POINT(24.01416666667 49.83)", +"http://www.yso.fi/onto/yso/p139883":"POINT(23.13622 63.82897)", +"http://www.yso.fi/onto/yso/p139884":"POINT(27.42553 63.16192)", +"http://www.yso.fi/onto/yso/p139885":"POINT(27.36769 63.1217)", +"http://www.yso.fi/onto/yso/p139886":"POINT(27.42666 63.09404)", +"http://www.yso.fi/onto/yso/p139887":"POINT(29.20866 61.89837)", +"http://www.yso.fi/onto/yso/p139888":"POINT(23.36981 63.81067)", +"http://www.yso.fi/onto/yso/p139889":"POINT(29.90857 64.4771)", +"http://www.yso.fi/onto/yso/p139890":"POINT(82.916666666667 55.033333333333)", +"http://www.yso.fi/onto/yso/p139891":"POINT(84.952222222222 56.488611111111)", +"http://www.yso.fi/onto/yso/p139892":"POINT(73.383333333333 54.966666666667)", +"http://www.yso.fi/onto/yso/p139893":"POINT(25.50534 65.06335)", +"http://www.yso.fi/onto/yso/p139895":"POINT(22.56143 63.21571)", +"http://www.yso.fi/onto/yso/p139896":"POINT(21.61741 63.11544)", +"http://www.yso.fi/onto/yso/p139897":"POINT(23.56367 63.80701)", +"http://www.yso.fi/onto/yso/p139898":"POINT(23.61568 63.77829)", +"http://www.yso.fi/onto/yso/p139899":"POINT(22.47053 60.4979)", +"http://www.yso.fi/onto/yso/p139901":"POINT(29.27368 62.61734)", +"http://www.yso.fi/onto/yso/p139902":"POINT(26.47266 63.64554)", +"http://www.yso.fi/onto/yso/p139903":"POINT(25.43223 64.24586)", +"http://www.yso.fi/onto/yso/p139904":"POINT(26.47098 63.71863)", +"http://www.yso.fi/onto/yso/p139905":"POINT(12.7 56.05)", +"http://www.yso.fi/onto/yso/p139906":"POINT(14.15 56.033333333333)", +"http://www.yso.fi/onto/yso/p139907":"POINT(21.564444444444 57.389722222222)", +"http://www.yso.fi/onto/yso/p139954":"POINT(18.5167 67.9)", +"http://www.yso.fi/onto/yso/p140484":"POINT(24.51401 60.13756)", +"http://www.yso.fi/onto/yso/p140485":"POINT(26.07311 63.00844)", +"http://www.yso.fi/onto/yso/p140490":"POINT(22.2208 60.98654)", +"http://www.yso.fi/onto/yso/p140610":"", +"http://www.yso.fi/onto/yso/p140939":"POINT(24.68249 60.1509)", +"http://www.yso.fi/onto/yso/p140940":"POINT(24.67333 60.14986)", +"http://www.yso.fi/onto/yso/p140941":"POINT(22.85077 61.33295)", +"http://www.yso.fi/onto/yso/p140942":"POINT(29.333333333333 60.688888888889)", +"http://www.yso.fi/onto/yso/p140945":"POINT(23.18825 61.14921)", +"http://www.yso.fi/onto/yso/p140946":"POINT(24.55678 65.75597)", +"http://www.yso.fi/onto/yso/p140947":"POINT(24.00756 60.21135)", +"http://www.yso.fi/onto/yso/p140948":"POINT(24.85927 66.84398)", +"http://www.yso.fi/onto/yso/p140949":"POINT(22.71411 62.58029)", +"http://www.yso.fi/onto/yso/p140950":"POINT(24.21266 63.41274)", +"http://www.yso.fi/onto/yso/p140951":"POINT(27.62649 63.56358)", +"http://www.yso.fi/onto/yso/p140952":"POINT(4.4 51.216666666667)", +"http://www.yso.fi/onto/yso/p140962":"", +"http://www.yso.fi/onto/yso/p140962":"POINT(24.479469 64.684828)", +"http://www.yso.fi/onto/yso/p140964":"POINT(23.66448 64.05237)", +"http://www.yso.fi/onto/yso/p140967":"POINT(24.75357 60.34429)", +"http://www.yso.fi/onto/yso/p141227":"POINT(23.21163 60.86499)", +"http://www.yso.fi/onto/yso/p141709":"POINT(25.38562 60.4127)", +"http://www.yso.fi/onto/yso/p141735":"POINT(35.207108333333 32.851147222222)", +"http://www.yso.fi/onto/yso/p141757":"", +"http://www.yso.fi/onto/yso/p141757":"", +"http://www.yso.fi/onto/yso/p141759":"POINT(38.33 -3.417)", +"http://www.yso.fi/onto/yso/p141761":"", +"http://www.yso.fi/onto/yso/p141764":"", +"http://www.yso.fi/onto/yso/p141764":"", +"http://www.yso.fi/onto/yso/p141766":"POINT(22.80467 63.46451)", +"http://www.yso.fi/onto/yso/p141769":"POINT(54 69.75)", +"http://www.yso.fi/onto/yso/p141771":"POINT(24.2722 60.60882)", +"http://www.yso.fi/onto/yso/p141772":"POINT(24.24283 60.64087)", +"http://www.yso.fi/onto/yso/p141773":"POINT(27.60294 62.8898)", +"http://www.yso.fi/onto/yso/p141774":"POINT(23.94513 61.82409)", +"http://www.yso.fi/onto/yso/p142237":"POINT(24.936667 60.183333)", +"http://www.yso.fi/onto/yso/p142238":"POINT(24.9279 60.18918)", +"http://www.yso.fi/onto/yso/p142261":"POINT(21.66889 63.09236)", +"http://www.yso.fi/onto/yso/p142262":"POINT(24.86171 60.87453)", +"http://www.yso.fi/onto/yso/p142264":"POINT(27.12951 65.10119)", +"http://www.yso.fi/onto/yso/p142265":"POINT(27.28852 64.97028)", +"http://www.yso.fi/onto/yso/p142267":"POINT(26 48)", +"http://www.yso.fi/onto/yso/p142269":"POINT(29.6539 60.5828)", +"http://www.yso.fi/onto/yso/p142272":"POINT(25.55533 63.11534)", +"http://www.yso.fi/onto/yso/p142276":"POINT(26.24143 60.53977)", +"http://www.yso.fi/onto/yso/p142282":"POINT(21.31864 62.47323)", +"http://www.yso.fi/onto/yso/p142283":"POINT(21.3116 62.45277)", +"http://www.yso.fi/onto/yso/p142287":"POINT(20.62283 59.94647)", +"http://www.yso.fi/onto/yso/p142288":"POINT(27.25723 65.32265)", +"http://www.yso.fi/onto/yso/p142302":"", +"http://www.yso.fi/onto/yso/p142303":"POINT(24.72723 60.94623)", +"http://www.yso.fi/onto/yso/p142304":"POINT(24.65806 60.17791)", +"http://www.yso.fi/onto/yso/p142313":"POINT(26.31767 61.65488)", +"http://www.yso.fi/onto/yso/p142314":"POINT(26.32687 61.63308)", +"http://www.yso.fi/onto/yso/p142315":"POINT(26.20828 61.66783)", +"http://www.yso.fi/onto/yso/p142316":"POINT(24.04931 61.36134)", +"http://www.yso.fi/onto/yso/p142319":"", +"http://www.yso.fi/onto/yso/p142742":"POINT(24.11745 63.03188)", +"http://www.yso.fi/onto/yso/p143268":"POINT(26.22793 61.04439)", +"http://www.yso.fi/onto/yso/p143269":"POINT(26.33885 61.6422)", +"http://www.yso.fi/onto/yso/p143270":"POINT(96 35)", +"http://www.yso.fi/onto/yso/p143271":"POINT(26.36627 61.49823)", +"http://www.yso.fi/onto/yso/p143272":"POINT(27.57439 60.85411)", +"http://www.yso.fi/onto/yso/p143273":"POINT(27.45984 60.74699)", +"http://www.yso.fi/onto/yso/p143274":"POINT(27.46535 60.75376)", +"http://www.yso.fi/onto/yso/p143275":"POINT(23.50739 60.96162)", +"http://www.yso.fi/onto/yso/p143276":"POINT(23.51927 60.8104)", +"http://www.yso.fi/onto/yso/p143541":"POINT(11.85 45.733333333333)", +"http://www.yso.fi/onto/yso/p143576":"", +"http://www.yso.fi/onto/yso/p143576":"POINT(26.981155 65.366415)", +"http://www.yso.fi/onto/yso/p144417":"", +"http://www.yso.fi/onto/yso/p144419":"", +"http://www.yso.fi/onto/yso/p144422":"POINT(29.719444444444 60.544444444444)", +"http://www.yso.fi/onto/yso/p144425":"", +"http://www.yso.fi/onto/yso/p144425":"", +"http://www.yso.fi/onto/yso/p144426":"POINT(26.72576 60.87302)", +"http://www.yso.fi/onto/yso/p144428":"POINT(26.67742 60.86711)", +"http://www.yso.fi/onto/yso/p144429":"", +"http://www.yso.fi/onto/yso/p144429":"POINT(24.5375 64.0722)", +"http://www.yso.fi/onto/yso/p144430":"POINT(27.95336 60.71317)", +"http://www.yso.fi/onto/yso/p144431":"POINT(27.98 60.7265)", +"http://www.yso.fi/onto/yso/p144432":"POINT(25.1191 60.22513)", +"http://www.yso.fi/onto/yso/p144433":"POINT(25.30871 65.21916)", +"http://www.yso.fi/onto/yso/p144434":"POINT(22.22393 60.46558)", +"http://www.yso.fi/onto/yso/p144435":"POINT(25.6884 62.33714)", +"http://www.yso.fi/onto/yso/p144437":"", +"http://www.yso.fi/onto/yso/p144437":"POINT(25.18962 61.86383)", +"http://www.yso.fi/onto/yso/p144438":"POINT(26.92199 64.23365)", +"http://www.yso.fi/onto/yso/p144439":"POINT(27.02107 64.25371)", +"http://www.yso.fi/onto/yso/p144440":"POINT(26.85934 64.23797)", +"http://www.yso.fi/onto/yso/p144441":"POINT(29.82137 62.53003)", +"http://www.yso.fi/onto/yso/p144442":"POINT(23.25 57.8)", +"http://www.yso.fi/onto/yso/p144443":"POINT(23.5945 60.09281)", +"http://www.yso.fi/onto/yso/p144444":"POINT(26.74932 60.42657)", +"http://www.yso.fi/onto/yso/p144445":"POINT(27.38226 60.46243)", +"http://www.yso.fi/onto/yso/p144446":"POINT(27.41466 60.40753)", +"http://www.yso.fi/onto/yso/p144621":"POINT(19.233333333333 50.033333333333)", +"http://www.yso.fi/onto/yso/p144622":"POINT(9.4811 51.2046)", +"http://www.yso.fi/onto/yso/p145045":"POINT(23.20881 60.43764)", +"http://www.yso.fi/onto/yso/p145051":"POINT(23.89013 62.39926)", +"http://www.yso.fi/onto/yso/p145052":"POINT(25.24072 60.38915)", +"http://www.yso.fi/onto/yso/p145053":"POINT(27.86809 62.31795)", +"http://www.yso.fi/onto/yso/p145054":"POINT(27.70354 60.5794)", +"http://www.yso.fi/onto/yso/p145055":"POINT(27.8179 60.59909)", +"http://www.yso.fi/onto/yso/p145056":"POINT(22.84255 61.62549)", +"http://www.yso.fi/onto/yso/p145057":"POINT(24.3107 61.63752)", +"http://www.yso.fi/onto/yso/p145058":"POINT(21.84248 62.50147)", +"http://www.yso.fi/onto/yso/p145059":"POINT(21.32932 60.50258)", +"http://www.yso.fi/onto/yso/p145060":"POINT(21.36468 60.48673)", +"http://www.yso.fi/onto/yso/p145061":"POINT(22.486111111111 58.253333333333)", +"http://www.yso.fi/onto/yso/p145062":"POINT(21.725 61.60155)", +"http://www.yso.fi/onto/yso/p145400":"POINT(25.12144 60.17948)", +"http://www.yso.fi/onto/yso/p145407":"POINT(22.6755 63.67897)", +"http://www.yso.fi/onto/yso/p145408":"POINT(24.91065 60.15452)", +"http://www.yso.fi/onto/yso/p145409":"POINT(24.0165 61.15045)", +"http://www.yso.fi/onto/yso/p145410":"POINT(24.0165 61.15045)", +"http://www.yso.fi/onto/yso/p145412":"POINT(26.43271 60.79438)", +"http://www.yso.fi/onto/yso/p145413":"POINT(23.64666667 60.81333333)", +"http://www.yso.fi/onto/yso/p145414":"POINT(27.79811 61.31264)", +"http://www.yso.fi/onto/yso/p145415":"", +"http://www.yso.fi/onto/yso/p145422":"POINT(22.90431 62.83464)", +"http://www.yso.fi/onto/yso/p145424":"POINT(22.82537 62.84711)", +"http://www.yso.fi/onto/yso/p145425":"POINT(22.78754 62.84325)", +"http://www.yso.fi/onto/yso/p145426":"POINT(23.24573 60.68575)", +"http://www.yso.fi/onto/yso/p145430":"POINT(26.98562 63.63614)", +"http://www.yso.fi/onto/yso/p145431":"POINT(26.88816 63.66499)", +"http://www.yso.fi/onto/yso/p145585":"POINT(20.00261 60.01847)", +"http://www.yso.fi/onto/yso/p145609":"POINT(23.53018 63.44974)", +"http://www.yso.fi/onto/yso/p145724":"POINT(-15.5925 27.958611111111)", +"http://www.yso.fi/onto/yso/p145965":"POINT(21.52946 61.50192)", +"http://www.yso.fi/onto/yso/p145967":"POINT(25.645 60.95897)", +"http://www.yso.fi/onto/yso/p145968":"", +"http://www.yso.fi/onto/yso/p145968":"POINT(25.467138888889 65.014411111111)", +"http://www.yso.fi/onto/yso/p145969":"POINT(27.34266 63.00673)", +"http://www.yso.fi/onto/yso/p145970":"POINT(29.43385 62.73499)", +"http://www.yso.fi/onto/yso/p145971":"POINT(29.34514 62.77858)", +"http://www.yso.fi/onto/yso/p145972":"POINT(30.32222 62.40522)", +"http://www.yso.fi/onto/yso/p145973":"POINT(30.29705 62.43987)", +"http://www.yso.fi/onto/yso/p145974":"POINT(24.34459 60.58443)", +"http://www.yso.fi/onto/yso/p145975":"POINT(24.3473 60.57141)", +"http://www.yso.fi/onto/yso/p145976":"POINT(24.48405 61.39035)", +"http://www.yso.fi/onto/yso/p145977":"POINT(28.86088 62.10941)", +"http://www.yso.fi/onto/yso/p145978":"POINT(24.04368 65.96183)", +"http://www.yso.fi/onto/yso/p145979":"POINT(24.88049 60.17465)", +"http://www.yso.fi/onto/yso/p145980":"POINT(24.83264 60.19698)", +"http://www.yso.fi/onto/yso/p146153":"POINT(76 34.5)", +"http://www.yso.fi/onto/yso/p146519":"POINT(25.14876 61.9801)", +"http://www.yso.fi/onto/yso/p146520":"POINT(22.20316 61.77665)", +"http://www.yso.fi/onto/yso/p146521":"POINT(26.60341 67.50934)", +"http://www.yso.fi/onto/yso/p146522":"POINT(27.34452 60.5135)", +"http://www.yso.fi/onto/yso/p146523":"POINT(23.26137 60.23737)", +"http://www.yso.fi/onto/yso/p146524":"POINT(23.2768 61.14362)", +"http://www.yso.fi/onto/yso/p146526":"POINT(21.59071 61.08542)", +"http://www.yso.fi/onto/yso/p146527":"POINT(21.56352 61.07669)", +"http://www.yso.fi/onto/yso/p146529":"POINT(25.01559 60.30038)", +"http://www.yso.fi/onto/yso/p146530":"POINT(25.5374 61.99233)", +"http://www.yso.fi/onto/yso/p146531":"POINT(25.38975 62.23317)", +"http://www.yso.fi/onto/yso/p146537":"POINT(26.92409 60.47246)", +"http://www.yso.fi/onto/yso/p146538":"POINT(26.59365 60.9752)", +"http://www.yso.fi/onto/yso/p146540":"POINT(25.4803 65.02006)", +"http://www.yso.fi/onto/yso/p146541":"POINT(24.20997 60.01068)", +"http://www.yso.fi/onto/yso/p146542":"POINT(23.24104 60.4924)", +"http://www.yso.fi/onto/yso/p146543":"", +"http://www.yso.fi/onto/yso/p146543":"", +"http://www.yso.fi/onto/yso/p146544":"POINT(27.84117 65.27691)", +"http://www.yso.fi/onto/yso/p146599":"POINT(27.52998 62.19956)", +"http://www.yso.fi/onto/yso/p146880":"POINT(23.73564 61.33646)", +"http://www.yso.fi/onto/yso/p146881":"POINT(17.55 58.9)", +"http://www.yso.fi/onto/yso/p146883":"POINT(26.48988 61.09004)", +"http://www.yso.fi/onto/yso/p146884":"POINT(27.25906 61.67335)", +"http://www.yso.fi/onto/yso/p147031":"POINT(12.581944 43.281667)", +"http://www.yso.fi/onto/yso/p147118":"POINT(25.49814 65.05151)", +"http://www.yso.fi/onto/yso/p147119":"POINT(25.50707 65.04418)", +"http://www.yso.fi/onto/yso/p147451":"POINT(11.331388888889 43.318333333333)", +"http://www.yso.fi/onto/yso/p147454":"POINT(23.41034 60.33588)", +"http://www.yso.fi/onto/yso/p147460":"POINT(26.40546 62.455)", +"http://www.yso.fi/onto/yso/p147462":"POINT(26.46017 62.51224)", +"http://www.yso.fi/onto/yso/p147463":"POINT(26.50323 62.46634)", +"http://www.yso.fi/onto/yso/p147465":"POINT(26.19091 65.03839)", +"http://www.yso.fi/onto/yso/p147471":"POINT(25.22235 64.42265)", +"http://www.yso.fi/onto/yso/p147472":"POINT(25.24327 64.36137)", +"http://www.yso.fi/onto/yso/p147475":"POINT(25.13684 64.46425)", +"http://www.yso.fi/onto/yso/p147477":"POINT(21.85617 61.06355)", +"http://www.yso.fi/onto/yso/p147478":"POINT(21.7668 61.07524)", +"http://www.yso.fi/onto/yso/p147479":"POINT(21.75049 61.15021)", +"http://www.yso.fi/onto/yso/p147481":"POINT(24.69171 60.14473)", +"http://www.yso.fi/onto/yso/p147482":"POINT(27.67842 62.90764)", +"http://www.yso.fi/onto/yso/p147483":"POINT(27.70848 62.90191)", +"http://www.yso.fi/onto/yso/p147484":"POINT(24.50075 60.95695)", +"http://www.yso.fi/onto/yso/p147485":"POINT(26.83665 61.27534)", +"http://www.yso.fi/onto/yso/p147486":"POINT(25.88661 62.36348)", +"http://www.yso.fi/onto/yso/p147487":"POINT(25.42109 62.09787)", +"http://www.yso.fi/onto/yso/p147488":"POINT(25.88046 62.00023)", +"http://www.yso.fi/onto/yso/p147489":"POINT(7.0130555555556 51.450833333333)", +"http://www.yso.fi/onto/yso/p147522":"POINT(23.20565 61.64991)", +"http://www.yso.fi/onto/yso/p147577":"POINT(-67.289166666667 -55.98)", +"http://www.yso.fi/onto/yso/p147660":"POINT(29.916666666667 31.2)", +"http://www.yso.fi/onto/yso/p147684":"POINT(26.56594 60.62201)", +"http://www.yso.fi/onto/yso/p147889":"POINT(95.316666666667 5.55)", +"http://www.yso.fi/onto/yso/p147997":"POINT(-70 -32.65)", +"http://www.yso.fi/onto/yso/p148017":"POINT(24.15183 60.93906)", +"http://www.yso.fi/onto/yso/p148018":"POINT(24.15506 60.93596)", +"http://www.yso.fi/onto/yso/p148019":"POINT(23.92148 61.01122)", +"http://www.yso.fi/onto/yso/p148020":"POINT(28.17289 61.07266)", +"http://www.yso.fi/onto/yso/p148021":"POINT(28.04134 60.91738)", +"http://www.yso.fi/onto/yso/p148022":"POINT(24.19815 62.0051)", +"http://www.yso.fi/onto/yso/p148023":"POINT(24.28753 61.90858)", +"http://www.yso.fi/onto/yso/p148024":"POINT(25.19783 62.43619)", +"http://www.yso.fi/onto/yso/p148025":"POINT(22.27088 60.43689)", +"http://www.yso.fi/onto/yso/p148026":"POINT(25.51799 64.99206)", +"http://www.yso.fi/onto/yso/p148027":"POINT(22.25169 60.48919)", +"http://www.yso.fi/onto/yso/p148028":"POINT(25.74764 62.20955)", +"http://www.yso.fi/onto/yso/p148029":"POINT(25.51914 65.01304)", +"http://www.yso.fi/onto/yso/p148030":"POINT(22.09718 60.4684)", +"http://www.yso.fi/onto/yso/p148033":"POINT(-59.316667 5.733333)", +"http://www.yso.fi/onto/yso/p148034":"POINT(38.933333333333 61.5)", +"http://www.yso.fi/onto/yso/p148035":"POINT(23.80996 61.48403)", +"http://www.yso.fi/onto/yso/p148036":"POINT(23.6684 61.5044)", +"http://www.yso.fi/onto/yso/p148037":"POINT(23.88788 61.5185)", +"http://www.yso.fi/onto/yso/p148038":"", +"http://www.yso.fi/onto/yso/p148040":"POINT(23.64963 61.51011)", +"http://www.yso.fi/onto/yso/p148042":"POINT(22.64406 60.69096)", +"http://www.yso.fi/onto/yso/p148142":"POINT(23.59597 61.50999)", +"http://www.yso.fi/onto/yso/p148237":"POINT(20.68693 60.20292)", +"http://www.yso.fi/onto/yso/p148662":"POINT(25.22435 61.353)", +"http://www.yso.fi/onto/yso/p148695":"POINT(29.78136 63.11763)", +"http://www.yso.fi/onto/yso/p148764":"POINT(21.805 61.5257)", +"http://www.yso.fi/onto/yso/p148765":"POINT(21.81416667 61.52861111)", +"http://www.yso.fi/onto/yso/p148834":"POINT(25.66947 63.47372)", +"http://www.yso.fi/onto/yso/p148835":"POINT(23.32845 60.47465)", +"http://www.yso.fi/onto/yso/p148837":"POINT(22.46906 60.56445)", +"http://www.yso.fi/onto/yso/p148839":"POINT(24.86057 60.17361)", +"http://www.yso.fi/onto/yso/p148841":"POINT(24.86398 60.18693)", +"http://www.yso.fi/onto/yso/p148842":"POINT(26.91508 60.91176)", +"http://www.yso.fi/onto/yso/p148988":"POINT(26.28718 64.95359)", +"http://www.yso.fi/onto/yso/p149096":"POINT(30.316666666667 59.95)", +"http://www.yso.fi/onto/yso/p149149":"POINT(-75.53846111 39.73712222)", +"http://www.yso.fi/onto/yso/p149164":"POINT(72.833333333333 18.966666666667)", +"http://www.yso.fi/onto/yso/p149165":"POINT(77.25 11.18)", +"http://www.yso.fi/onto/yso/p149166":"POINT(77.597963 12.969907)", +"http://www.yso.fi/onto/yso/p149167":"POINT(78.466666666667 17.366666666667)", +"http://www.yso.fi/onto/yso/p149168":"POINT(73.847777777778 18.523611111111)", +"http://www.yso.fi/onto/yso/p149170":"POINT(85.4 22.12)", +"http://www.yso.fi/onto/yso/p149171":"POINT(80.27 13.09)", +"http://www.yso.fi/onto/yso/p149172":"POINT(27.23348 60.53684)", +"http://www.yso.fi/onto/yso/p149173":"POINT(27.2061 60.5518)", +"http://www.yso.fi/onto/yso/p149174":"POINT(27.20315 60.56028)", +"http://www.yso.fi/onto/yso/p149175":"POINT(25.59029 60.33646)", +"http://www.yso.fi/onto/yso/p149176":"POINT(23.87951 66.5635)", +"http://www.yso.fi/onto/yso/p149177":"POINT(23.90177 66.5161)", +"http://www.yso.fi/onto/yso/p149178":"POINT(23.88433 66.52994)", +"http://www.yso.fi/onto/yso/p149179":"POINT(23.52132 60.30071)", +"http://www.yso.fi/onto/yso/p149180":"POINT(24.59418 64.25972)", +"http://www.yso.fi/onto/yso/p149181":"POINT(28.16919 63.92393)", +"http://www.yso.fi/onto/yso/p149182":"POINT(26.59665 60.87799)", +"http://www.yso.fi/onto/yso/p149185":"POINT(24.73166 60.30454)", +"http://www.yso.fi/onto/yso/p149186":"", +"http://www.yso.fi/onto/yso/p149186":"", +"http://www.yso.fi/onto/yso/p149187":"", +"http://www.yso.fi/onto/yso/p149187":"", +"http://www.yso.fi/onto/yso/p149205":"POINT(27.54568 67.21332)", +"http://www.yso.fi/onto/yso/p149206":"POINT(28.32577 67.2524)", +"http://www.yso.fi/onto/yso/p149207":"POINT(28.97343 67.01868)", +"http://www.yso.fi/onto/yso/p149208":"POINT(28.79278 67.07194)", +"http://www.yso.fi/onto/yso/p149209":"POINT(28.73302 67.16175)", +"http://www.yso.fi/onto/yso/p149302":"POINT(22.39118 60.35629)", +"http://www.yso.fi/onto/yso/p149322":"POINT(1 42.666666666667)", +"http://www.yso.fi/onto/yso/p149420":"POINT(29 41)", +"http://www.yso.fi/onto/yso/p149458":"", +"http://www.yso.fi/onto/yso/p149464":"POINT(-4 37)", +"http://www.yso.fi/onto/yso/p149659":"POINT(29.9 60.5)", +"http://www.yso.fi/onto/yso/p149712":"POINT(23.657088888889 66.39025)", +"http://www.yso.fi/onto/yso/p149713":"POINT(25.89549 61.7004)", +"http://www.yso.fi/onto/yso/p149714":"POINT(25.89213 61.73122)", +"http://www.yso.fi/onto/yso/p149715":"POINT(25.92966 61.74529)", +"http://www.yso.fi/onto/yso/p149716":"POINT(26.00114 61.61283)", +"http://www.yso.fi/onto/yso/p149717":"POINT(25.9604 61.7041)", +"http://www.yso.fi/onto/yso/p149718":"POINT(27.51754 63.54041)", +"http://www.yso.fi/onto/yso/p149720":"POINT(21.60096 63.20055)", +"http://www.yso.fi/onto/yso/p149722":"POINT(21.85459 63.02624)", +"http://www.yso.fi/onto/yso/p149723":"POINT(27.70542 64.12085)", +"http://www.yso.fi/onto/yso/p149724":"POINT(34.099927777778 44.933347222222)", +"http://www.yso.fi/onto/yso/p149730":"POINT(25.43102 60.16901)", +"http://www.yso.fi/onto/yso/p149743":"POINT(24.27099 61.10567)", +"http://www.yso.fi/onto/yso/p149748":"POINT(26.78253 62.79753)", +"http://www.yso.fi/onto/yso/p149749":"POINT(26.8751 62.71342)", +"http://www.yso.fi/onto/yso/p150081":"POINT(80 -20)", +"http://www.yso.fi/onto/yso/p150235":"POINT(22.066667 63.216667)", +"http://www.yso.fi/onto/yso/p150272":"POINT(25.37812 61.56646)", +"http://www.yso.fi/onto/yso/p150274":"POINT(25.29272 61.58152)", +"http://www.yso.fi/onto/yso/p150275":"POINT(29.66591 62.6457)", +"http://www.yso.fi/onto/yso/p150277":"POINT(16.127178 -12.653801)", +"http://www.yso.fi/onto/yso/p150278":"POINT(-28 38.5)", +"http://www.yso.fi/onto/yso/p150283":"POINT(27.96337 62.10122)", +"http://www.yso.fi/onto/yso/p150284":"POINT(29.41973 63.17696)", +"http://www.yso.fi/onto/yso/p150286":"POINT(29.73084 62.41117)", +"http://www.yso.fi/onto/yso/p150288":"POINT(12.25 57.105555555556)", +"http://www.yso.fi/onto/yso/p150290":"POINT(25.09345 63.29699)", +"http://www.yso.fi/onto/yso/p150291":"POINT(25.12557 63.34551)", +"http://www.yso.fi/onto/yso/p150311":"POINT(27.45811 60.69223)", +"http://www.yso.fi/onto/yso/p150376":"POINT(-18.226111111111 64.064722222222)", +"http://www.yso.fi/onto/yso/p150503":"POINT(25.12393 66.35887)", +"http://www.yso.fi/onto/yso/p150750":"POINT(28.25051 65.58189)", +"http://www.yso.fi/onto/yso/p150753":"POINT(26.31006 65.43404)", +"http://www.yso.fi/onto/yso/p150756":"POINT(23.06571 60.392869)", +"http://www.yso.fi/onto/yso/p150758":"POINT(25.80206 61.34413)", +"http://www.yso.fi/onto/yso/p150761":"POINT(25.50855 64.85988)", +"http://www.yso.fi/onto/yso/p150762":"POINT(27.63692 61.24704)", +"http://www.yso.fi/onto/yso/p150936":"POINT(23.05768 60.39917)", +"http://www.yso.fi/onto/yso/p151150":"POINT(-5.7166666666667 -15.95)", +"http://www.yso.fi/onto/yso/p151407":"POINT(16.5 40.5)", +"http://www.yso.fi/onto/yso/p151620":"POINT(46.883333333333 43.1)", +"http://www.yso.fi/onto/yso/p151651":"POINT(12.533055555556 57.930277777778)", +"http://www.yso.fi/onto/yso/p151679":"POINT(24.85263 67.87308)", +"http://www.yso.fi/onto/yso/p151680":"POINT(24.95712 67.86635)", +"http://www.yso.fi/onto/yso/p151681":"POINT(22.14074 61.61932)", +"http://www.yso.fi/onto/yso/p151682":"POINT(34.566666666667 68.033333333333)", +"http://www.yso.fi/onto/yso/p151687":"POINT(29.18608 66.14288)", +"http://www.yso.fi/onto/yso/p151688":"POINT(29.21836 66.14055)", +"http://www.yso.fi/onto/yso/p151689":"", +"http://www.yso.fi/onto/yso/p151689":"POINT(25.74738 62.24264)", +"http://www.yso.fi/onto/yso/p151841":"POINT(-17.866666666667 28.666666666667)", +"http://www.yso.fi/onto/yso/p151842":"POINT(-18 27.75)", +"http://www.yso.fi/onto/yso/p151867":"POINT(27.31879 61.54312)", +"http://www.yso.fi/onto/yso/p151882":"POINT(22.94307 59.80083)", +"http://www.yso.fi/onto/yso/p151902":"POINT(23.46569 61.36869)", +"http://www.yso.fi/onto/yso/p151903":"POINT(23.45783 61.34945)", +"http://www.yso.fi/onto/yso/p152128":"POINT(31.0069727 59.9484016)", +"http://www.yso.fi/onto/yso/p152142":"POINT(23.265 58.8251)", +"http://www.yso.fi/onto/yso/p152144":"POINT(24.5309 60.15621)", +"http://www.yso.fi/onto/yso/p152145":"POINT(24.54781 60.17908)", +"http://www.yso.fi/onto/yso/p152146":"POINT(25.9754 61.99727)", +"http://www.yso.fi/onto/yso/p152160":"POINT(24.38516 61.00461)", +"http://www.yso.fi/onto/yso/p152163":"POINT(31.75 60.05)", +"http://www.yso.fi/onto/yso/p152165":"POINT(24.8681 60.95727)", +"http://www.yso.fi/onto/yso/p152166":"POINT(24.83931 60.96821)", +"http://www.yso.fi/onto/yso/p152167":"POINT(30.005 64.78219)", +"http://www.yso.fi/onto/yso/p152168":"POINT(24.64858 60.25358)", +"http://www.yso.fi/onto/yso/p152169":"POINT(24.68325 60.29339)", +"http://www.yso.fi/onto/yso/p152170":"POINT(28.41823 61.56232)", +"http://www.yso.fi/onto/yso/p152171":"POINT(24.34408 61.25908)", +"http://www.yso.fi/onto/yso/p152172":"POINT(24.46447 61.27513)", +"http://www.yso.fi/onto/yso/p152189":"POINT(24.86885 60.17247)", +"http://www.yso.fi/onto/yso/p152455":"POINT(100.87194444444 12.923055555556)", +"http://www.yso.fi/onto/yso/p152514":"POINT(24.75 60.23)", +"http://www.yso.fi/onto/yso/p152584":"POINT(27.57727 62.90102)", +"http://www.yso.fi/onto/yso/p152587":"POINT(27.30053 66.48398)", +"http://www.yso.fi/onto/yso/p152588":"POINT(27.30306 66.38396)", +"http://www.yso.fi/onto/yso/p152589":"POINT(27.30665 66.39233)", +"http://www.yso.fi/onto/yso/p152590":"POINT(24.50312 60.05416)", +"http://www.yso.fi/onto/yso/p152592":"POINT(23.92388 60.19511)", +"http://www.yso.fi/onto/yso/p152595":"POINT(23.35379 60.19336)", +"http://www.yso.fi/onto/yso/p152596":"POINT(24.19383 64.29862)", +"http://www.yso.fi/onto/yso/p152597":"POINT(24.00275 61.00335)", +"http://www.yso.fi/onto/yso/p152598":"POINT(23.0073 60.95501)", +"http://www.yso.fi/onto/yso/p152600":"POINT(21.74365 61.47952)", +"http://www.yso.fi/onto/yso/p152603":"POINT(10.314722 36.886667)", +"http://www.yso.fi/onto/yso/p152605":"POINT(7.3 35.5)", +"http://www.yso.fi/onto/yso/p152606":"POINT(21.2146 32.530316)", +"http://www.yso.fi/onto/yso/p152608":"POINT(28.05075 64.42259)", +"http://www.yso.fi/onto/yso/p152611":"POINT(27.0841 61.64488)", +"http://www.yso.fi/onto/yso/p152613":"POINT(23.82442 61.47037)", +"http://www.yso.fi/onto/yso/p152614":"POINT(23.81858 61.47016)", +"http://www.yso.fi/onto/yso/p152617":"POINT(22.82873 63.09473)", +"http://www.yso.fi/onto/yso/p152618":"POINT(29.53308 63.91402)", +"http://www.yso.fi/onto/yso/p153010":"POINT(30.416666666667 59.716666666667)", +"http://www.yso.fi/onto/yso/p153011":"POINT(26.58579 60.84423)", +"http://www.yso.fi/onto/yso/p153014":"POINT(24.67701 68.00211)", +"http://www.yso.fi/onto/yso/p153015":"POINT(28.47035 61.12536)", +"http://www.yso.fi/onto/yso/p153016":"POINT(24.75887 60.52692)", +"http://www.yso.fi/onto/yso/p153017":"POINT(24.06799 65.97406)", +"http://www.yso.fi/onto/yso/p153018":"POINT(21.72601 62.89198)", +"http://www.yso.fi/onto/yso/p153019":"POINT(25.44028 60.75323)", +"http://www.yso.fi/onto/yso/p153023":"POINT(25.314 61.01138)", +"http://www.yso.fi/onto/yso/p153106":"POINT(24.45046 60.97039)", +"http://www.yso.fi/onto/yso/p153107":"POINT(24.46094 60.97607)", +"http://www.yso.fi/onto/yso/p153124":"POINT(25.06783 60.41146)", +"http://www.yso.fi/onto/yso/p153295":"POINT(136.93333333333 35.116666666667)", +"http://www.yso.fi/onto/yso/p153296":"POINT(-68.673333333333 -16.554722222222)", +"http://www.yso.fi/onto/yso/p153297":"POINT(-68.606944444444 -17.023888888889)", +"http://www.yso.fi/onto/yso/p153298":"POINT(25.616666666667 57.333333333333)", +"http://www.yso.fi/onto/yso/p153300":"POINT(23.68397 61.34029)", +"http://www.yso.fi/onto/yso/p153313":"POINT(25 13)", +"http://www.yso.fi/onto/yso/p153717":"POINT(22.41899 59.854)", +"http://www.yso.fi/onto/yso/p153727":"POINT(22.2735 60.28176)", +"http://www.yso.fi/onto/yso/p153728":"POINT(22.26819 60.27505)", +"http://www.yso.fi/onto/yso/p153736":"POINT(25.4828 65.0105)", +"http://www.yso.fi/onto/yso/p153737":"POINT(26.21382 67.4153)", +"http://www.yso.fi/onto/yso/p153779":"POINT(24.96775 60.18137)", +"http://www.yso.fi/onto/yso/p153807":"POINT(25.13488 61.83077)", +"http://www.yso.fi/onto/yso/p153808":"POINT(25.05096 61.99445)", +"http://www.yso.fi/onto/yso/p153826":"POINT(26.03616 60.68185)", +"http://www.yso.fi/onto/yso/p153827":"POINT(22.67517 60.94144)", +"http://www.yso.fi/onto/yso/p153828":"POINT(24.79889 60.26023)", +"http://www.yso.fi/onto/yso/p153829":"POINT(24.80333333 60.25833333)", +"http://www.yso.fi/onto/yso/p153834":"POINT(23.77113 62.85052)", +"http://www.yso.fi/onto/yso/p153835":"POINT(24.68783 60.25318)", +"http://www.yso.fi/onto/yso/p153836":"POINT(24.70895 60.41191)", +"http://www.yso.fi/onto/yso/p153837":"", +"http://www.yso.fi/onto/yso/p153838":"", +"http://www.yso.fi/onto/yso/p153839":"POINT(23.21369 62.89661)", +"http://www.yso.fi/onto/yso/p153841":"POINT(25.67527 60.66323)", +"http://www.yso.fi/onto/yso/p153842":"POINT(21.81178 61.48582)", +"http://www.yso.fi/onto/yso/p153845":"POINT(25.00474 60.30853)", +"http://www.yso.fi/onto/yso/p153846":"POINT(25.13718 60.28659)", +"http://www.yso.fi/onto/yso/p153855":"POINT(25.94969 60.50285)", +"http://www.yso.fi/onto/yso/p153866":"POINT(24.56288 61.57756)", +"http://www.yso.fi/onto/yso/p153867":"POINT(24.1319 62.15631)", +"http://www.yso.fi/onto/yso/p153887":"POINT(24.36113 66.33675)", +"http://www.yso.fi/onto/yso/p153890":"POINT(24.73615 60.14466)", +"http://www.yso.fi/onto/yso/p153988":"POINT(22.34559 60.51309)", +"http://www.yso.fi/onto/yso/p153989":"POINT(26.69162 60.71188)", +"http://www.yso.fi/onto/yso/p154028":"POINT(22.70779 62.34783)", +"http://www.yso.fi/onto/yso/p154031":"POINT(25.31204 64.07926)", +"http://www.yso.fi/onto/yso/p154133":"POINT(22.58715 62.91205)", +"http://www.yso.fi/onto/yso/p154134":"POINT(123.42583333333 41.803888888889)", +"http://www.yso.fi/onto/yso/p154135":"POINT(21.87728 61.42671)", +"http://www.yso.fi/onto/yso/p154136":"POINT(22.6754 61.86663)", +"http://www.yso.fi/onto/yso/p154137":"POINT(21.75 60.51111111)", +"http://www.yso.fi/onto/yso/p154138":"POINT(24.98242 60.25995)", +"http://www.yso.fi/onto/yso/p154139":"POINT(25.00858 60.25425)", +"http://www.yso.fi/onto/yso/p154141":"POINT(21.35129 62.68538)", +"http://www.yso.fi/onto/yso/p154142":"POINT(27.99481 64.39245)", +"http://www.yso.fi/onto/yso/p154144":"POINT(21.8493 61.51107)", +"http://www.yso.fi/onto/yso/p154145":"POINT(22.58652 63.05557)", +"http://www.yso.fi/onto/yso/p154147":"POINT(23.09319 63.59476)", +"http://www.yso.fi/onto/yso/p154148":"POINT(23.09319 63.59476)", +"http://www.yso.fi/onto/yso/p154152":"POINT(21.56875 62.24505)", +"http://www.yso.fi/onto/yso/p154154":"POINT(23.18406 63.5236)", +"http://www.yso.fi/onto/yso/p154180":"POINT(24.44995 62.03965)", +"http://www.yso.fi/onto/yso/p154204":"POINT(-98 35.5)", +"http://www.yso.fi/onto/yso/p154224":"POINT(26.63366 62.79352)", +"http://www.yso.fi/onto/yso/p154439":"POINT(30.6461 59.93)", +"http://www.yso.fi/onto/yso/p154440":"POINT(31.345402 59.796548)", +"http://www.yso.fi/onto/yso/p154441":"POINT(29.979902 59.636861)", +"http://www.yso.fi/onto/yso/p154767":"POINT(24.95 60.3)", +"http://www.yso.fi/onto/yso/p155154":"POINT(27.69475 60.51352)", +"http://www.yso.fi/onto/yso/p155155":"POINT(27.71814 60.51552)", +"http://www.yso.fi/onto/yso/p155156":"POINT(28.81708 67.08889)", +"http://www.yso.fi/onto/yso/p155157":"POINT(30.7556 63.25151)", +"http://www.yso.fi/onto/yso/p155158":"POINT(27.24204 60.54161)", +"http://www.yso.fi/onto/yso/p155159":"POINT(27.2001 60.58409)", +"http://www.yso.fi/onto/yso/p155160":"POINT(22.50298 63.39209)", +"http://www.yso.fi/onto/yso/p155161":"POINT(25.72324 62.23533)", +"http://www.yso.fi/onto/yso/p155162":"POINT(25.07361 60.16279)", +"http://www.yso.fi/onto/yso/p155163":"POINT(25.082 60.1556)", +"http://www.yso.fi/onto/yso/p155164":"POINT(23.12501 60.37867)", +"http://www.yso.fi/onto/yso/p155165":"POINT(23.18345 63.09223)", +"http://www.yso.fi/onto/yso/p155166":"POINT(27.02648 64.55863)", +"http://www.yso.fi/onto/yso/p155167":"POINT(-2.9994438888889 16.773333055556)", +"http://www.yso.fi/onto/yso/p155634":"POINT(27.64714 62.88914)", +"http://www.yso.fi/onto/yso/p155637":"POINT(24.59861 61.62242)", +"http://www.yso.fi/onto/yso/p155648":"POINT(14.258888888889 36.045)", +"http://www.yso.fi/onto/yso/p155717":"POINT(30.83415 62.3966)", +"http://www.yso.fi/onto/yso/p155754":"POINT(24.45077 60.25207)", +"http://www.yso.fi/onto/yso/p156060":"POINT(27.9615 62.96338)", +"http://www.yso.fi/onto/yso/p156064":"POINT(27.97385 63.03273)", +"http://www.yso.fi/onto/yso/p156065":"POINT(23.69833 61.81867)", +"http://www.yso.fi/onto/yso/p156066":"POINT(24.005 47.672777777778)", +"http://www.yso.fi/onto/yso/p156067":"POINT(17.3 59.05)", +"http://www.yso.fi/onto/yso/p156069":"POINT(27.91539 63.07233)", +"http://www.yso.fi/onto/yso/p156070":"POINT(27.91185 63.04218)", +"http://www.yso.fi/onto/yso/p156071":"POINT(23.82203 61.49347)", +"http://www.yso.fi/onto/yso/p156073":"POINT(24.21509 60.50408)", +"http://www.yso.fi/onto/yso/p156074":"POINT(30.60373 62.17562)", +"http://www.yso.fi/onto/yso/p156076":"POINT(24.88589 60.26692)", +"http://www.yso.fi/onto/yso/p156077":"POINT(24.88589 60.26692)", +"http://www.yso.fi/onto/yso/p156078":"POINT(23.59366 61.45546)", +"http://www.yso.fi/onto/yso/p156080":"POINT(25.47953 60.89531)", +"http://www.yso.fi/onto/yso/p156081":"", +"http://www.yso.fi/onto/yso/p156082":"", +"http://www.yso.fi/onto/yso/p156083":"POINT(24.16247 62.74511)", +"http://www.yso.fi/onto/yso/p156084":"POINT(24.16672 60.55046)", +"http://www.yso.fi/onto/yso/p156085":"POINT(24.16672 60.55046)", +"http://www.yso.fi/onto/yso/p156086":"", +"http://www.yso.fi/onto/yso/p156087":"POINT(25.9756 60.45095)", +"http://www.yso.fi/onto/yso/p156088":"POINT(25.93194 60.48128)", +"http://www.yso.fi/onto/yso/p156090":"POINT(24.16268 60.26459)", +"http://www.yso.fi/onto/yso/p156514":"POINT(29.45751 63.82581)", +"http://www.yso.fi/onto/yso/p156515":"POINT(29.79684 62.65693)", +"http://www.yso.fi/onto/yso/p156516":"POINT(26.98788 61.00183)", +"http://www.yso.fi/onto/yso/p156519":"POINT(29.190277777778 60.684722222222)", +"http://www.yso.fi/onto/yso/p156522":"POINT(35.458611111111 31.740833333333)", +"http://www.yso.fi/onto/yso/p156524":"POINT(24.96358 60.24592)", +"http://www.yso.fi/onto/yso/p156525":"POINT(23.25782 61.18252)", +"http://www.yso.fi/onto/yso/p156526":"POINT(24.84209 62.76734)", +"http://www.yso.fi/onto/yso/p156527":"POINT(25.73163 62.25363)", +"http://www.yso.fi/onto/yso/p156528":"POINT(26.03472 67.33934)", +"http://www.yso.fi/onto/yso/p156529":"POINT(22.76667 61.2283)", +"http://www.yso.fi/onto/yso/p156596":"POINT(44.416666666667 33.35)", +"http://www.yso.fi/onto/yso/p156654":"POINT(22.27599 60.41335)", +"http://www.yso.fi/onto/yso/p156655":"POINT(22.20096 60.45989)", +"http://www.yso.fi/onto/yso/p157003":"POINT(27.21868 60.62552)", +"http://www.yso.fi/onto/yso/p157093":"", +"http://www.yso.fi/onto/yso/p157294":"POINT(19.75619 60.32789)", +"http://www.yso.fi/onto/yso/p157451":"POINT(24.7 61.33333333)", +"http://www.yso.fi/onto/yso/p157453":"POINT(27.22859 61.28072)", +"http://www.yso.fi/onto/yso/p157454":"POINT(30.50075 62.98921)", +"http://www.yso.fi/onto/yso/p157455":"POINT(22.30776 60.41803)", +"http://www.yso.fi/onto/yso/p157456":"POINT(22.30512 60.42121)", +"http://www.yso.fi/onto/yso/p157457":"POINT(18.108333333333 42.640277777778)", +"http://www.yso.fi/onto/yso/p157463":"POINT(23.17993 61.06879)", +"http://www.yso.fi/onto/yso/p157464":"POINT(22.15 65.6)", +"http://www.yso.fi/onto/yso/p157465":"POINT(11.216666666667 46.183333333333)", +"http://www.yso.fi/onto/yso/p157466":"POINT(29.75153 62.61538)", +"http://www.yso.fi/onto/yso/p157467":"POINT(28.41726 65.23108)", +"http://www.yso.fi/onto/yso/p157569":"POINT(44 56.333333333333)", +"http://www.yso.fi/onto/yso/p157607":"POINT(30.48589 62.69756)", +"http://www.yso.fi/onto/yso/p157796":"POINT(22.04131 62.94653)", +"http://www.yso.fi/onto/yso/p157797":"POINT(21.96968 62.94298)", +"http://www.yso.fi/onto/yso/p157809":"POINT(25.31512 62.06097)", +"http://www.yso.fi/onto/yso/p157905":"POINT(23.85487 61.5385)", +"http://www.yso.fi/onto/yso/p157931":"POINT(22.76184 60.69921)", +"http://www.yso.fi/onto/yso/p157943":"POINT(27.37808 63.26993)", +"http://www.yso.fi/onto/yso/p158024":"POINT(22.23453 62.32966)", +"http://www.yso.fi/onto/yso/p158111":"POINT(21.60002 60.11187)", +"http://www.yso.fi/onto/yso/p158112":"POINT(26.05328 62.52732)", +"http://www.yso.fi/onto/yso/p158115":"POINT(31.05045 62.73165)", +"http://www.yso.fi/onto/yso/p158118":"POINT(26.97979 60.46965)", +"http://www.yso.fi/onto/yso/p158121":"POINT(31.483333333333 61.566666666667)", +"http://www.yso.fi/onto/yso/p158122":"POINT(25.01332 60.24818)", +"http://www.yso.fi/onto/yso/p158394":"POINT(60.583333333333 56.833333333333)", +"http://www.yso.fi/onto/yso/p158713":"POINT(28.74815 65.33443)", +"http://www.yso.fi/onto/yso/p158714":"POINT(28.67108 65.34923)", +"http://www.yso.fi/onto/yso/p158715":"POINT(21.716666666667 47.95)", +"http://www.yso.fi/onto/yso/p158716":"POINT(22.88337 63.40104)", +"http://www.yso.fi/onto/yso/p158717":"POINT(22.88337 63.40104)", +"http://www.yso.fi/onto/yso/p158718":"POINT(22.5989 61.51505)", +"http://www.yso.fi/onto/yso/p158720":"POINT(25.97467 62.42457)", +"http://www.yso.fi/onto/yso/p158721":"POINT(25.86666679 62.5)", +"http://www.yso.fi/onto/yso/p158722":"POINT(25.70303 62.59551)", +"http://www.yso.fi/onto/yso/p158723":"POINT(22.92991 63.69368)", +"http://www.yso.fi/onto/yso/p158724":"POINT(104.06666666667 12.883333333333)", +"http://www.yso.fi/onto/yso/p158726":"POINT(104.91666666667 11.55)", +"http://www.yso.fi/onto/yso/p158728":"POINT(28.31495 63.25978)", +"http://www.yso.fi/onto/yso/p158729":"POINT(-4.625 36.541666666667)", +"http://www.yso.fi/onto/yso/p158730":"", +"http://www.yso.fi/onto/yso/p158731":"POINT(22.40182 63.32321)", +"http://www.yso.fi/onto/yso/p158732":"POINT(22.39579 63.31446)", +"http://www.yso.fi/onto/yso/p158736":"POINT(-84.083333333333 9.9333333333333)", +"http://www.yso.fi/onto/yso/p158737":"POINT(25.76475 60.97385)", +"http://www.yso.fi/onto/yso/p158738":"POINT(16.416666666667 47.5)", +"http://www.yso.fi/onto/yso/p158754":"POINT(32.89 24.091)", +"http://www.yso.fi/onto/yso/p158760":"POINT(38 22)", +"http://www.yso.fi/onto/yso/p158997":"POINT(24.4975 61.3375)", +"http://www.yso.fi/onto/yso/p159117":"POINT(13.4 52.516666666667)", +"http://www.yso.fi/onto/yso/p159503":"POINT(21.73212 61.93986)", +"http://www.yso.fi/onto/yso/p159504":"POINT(23.01787 63.15098)", +"http://www.yso.fi/onto/yso/p159506":"POINT(25.41454 65.0793)", +"http://www.yso.fi/onto/yso/p159507":"POINT(23.66714 61.47139)", +"http://www.yso.fi/onto/yso/p159508":"POINT(26.43324 63.88315)", +"http://www.yso.fi/onto/yso/p159509":"POINT(26.47942 63.83483)", +"http://www.yso.fi/onto/yso/p159510":"POINT(17.433333333333 59.175)", +"http://www.yso.fi/onto/yso/p159514":"POINT(24.11589 67.96665)", +"http://www.yso.fi/onto/yso/p159517":"POINT(25.42301 65.03245)", +"http://www.yso.fi/onto/yso/p159518":"POINT(25.46927 66.64788)", +"http://www.yso.fi/onto/yso/p159519":"POINT(25.4305 66.6211)", +"http://www.yso.fi/onto/yso/p159520":"POINT(16.4 61.35)", +"http://www.yso.fi/onto/yso/p159521":"POINT(16.507 61.512)", +"http://www.yso.fi/onto/yso/p159780":"POINT(22.91818 61.08425)", +"http://www.yso.fi/onto/yso/p159782":"POINT(22.6808 63.26317)", +"http://www.yso.fi/onto/yso/p159783":"POINT(110.00444444444 -7.4916666666667)", +"http://www.yso.fi/onto/yso/p159785":"", +"http://www.yso.fi/onto/yso/p159786":"POINT(106.8 -6.1666666666667)", +"http://www.yso.fi/onto/yso/p159787":"POINT(106.8 -6.1666666666667)", +"http://www.yso.fi/onto/yso/p159788":"POINT(24.94117 60.1879)", +"http://www.yso.fi/onto/yso/p159790":"POINT(21.35043 61.47763)", +"http://www.yso.fi/onto/yso/p159791":"POINT(22.70647 60.45967)", +"http://www.yso.fi/onto/yso/p159792":"POINT(23.03348 60.14319)", +"http://www.yso.fi/onto/yso/p159793":"POINT(25.73163 61.83441)", +"http://www.yso.fi/onto/yso/p159794":"POINT(25.82754 67.26608)", +"http://www.yso.fi/onto/yso/p159795":"POINT(22.76473 61.08373)", +"http://www.yso.fi/onto/yso/p159902":"POINT(27.916944444444 43.207777777778)", +"http://www.yso.fi/onto/yso/p159905":"POINT(125.30833333333 43.884166666667)", +"http://www.yso.fi/onto/yso/p159906":"POINT(129 48)", +"http://www.yso.fi/onto/yso/p159909":"POINT(23.8904 62.70688)", +"http://www.yso.fi/onto/yso/p159910":"POINT(23.76545 62.65974)", +"http://www.yso.fi/onto/yso/p159916":"POINT(25.91042 60.70292)", +"http://www.yso.fi/onto/yso/p159921":"POINT(22.42684 61.38787)", +"http://www.yso.fi/onto/yso/p159922":"POINT(29.0255 63.79026)", +"http://www.yso.fi/onto/yso/p159951":"", +"http://www.yso.fi/onto/yso/p160063":"POINT(24.90688 60.27076)", +"http://www.yso.fi/onto/yso/p160088":"POINT(27.56111 61.0406)", +"http://www.yso.fi/onto/yso/p160281":"POINT(21.56562 63.00468)", +"http://www.yso.fi/onto/yso/p160286":"POINT(25.595555555556 58.363333333333)", +"http://www.yso.fi/onto/yso/p160305":"POINT(24.43817 61.50697)", +"http://www.yso.fi/onto/yso/p160531":"POINT(28.51666667 38.35)", +"http://www.yso.fi/onto/yso/p160687":"POINT(20.90177 59.87367)", +"http://www.yso.fi/onto/yso/p160688":"POINT(22.2704 63.01488)", +"http://www.yso.fi/onto/yso/p160689":"POINT(27.73701 64.92685)", +"http://www.yso.fi/onto/yso/p160691":"POINT(61.333333333333 58.7)", +"http://www.yso.fi/onto/yso/p160692":"POINT(24.56935 60.18775)", +"http://www.yso.fi/onto/yso/p160693":"POINT(25.46041 65.01998)", +"http://www.yso.fi/onto/yso/p160696":"POINT(23.022 63.73115)", +"http://www.yso.fi/onto/yso/p160697":"POINT(24.71017 61.25597)", +"http://www.yso.fi/onto/yso/p160698":"POINT(-0.771 51.975)", +"http://www.yso.fi/onto/yso/p160699":"POINT(-0.63333333333333 52.65)", +"http://www.yso.fi/onto/yso/p160700":"POINT(-0.66666666666667 52.670555555556)", +"http://www.yso.fi/onto/yso/p160701":"POINT(-0.74 52.68)", +"http://www.yso.fi/onto/yso/p160703":"POINT(27.91882 70.08414)", +"http://www.yso.fi/onto/yso/p160716":"POINT(103.83333333333 13.433333333333)", +"http://www.yso.fi/onto/yso/p160717":"POINT(94.86667 21.16667)", +"http://www.yso.fi/onto/yso/p160804":"POINT(35.75 33)", +"http://www.yso.fi/onto/yso/p160824":"POINT(21.85103 61.19642)", +"http://www.yso.fi/onto/yso/p160912":"POINT(28.30583 68.84696)", +"http://www.yso.fi/onto/yso/p160950":"POINT(30.17037 62.17777)", +"http://www.yso.fi/onto/yso/p160951":"POINT(85.13 25.37)", +"http://www.yso.fi/onto/yso/p160952":"POINT(24.52259 62.87285)", +"http://www.yso.fi/onto/yso/p160954":"POINT(25.45164 66.70559)", +"http://www.yso.fi/onto/yso/p160955":"POINT(25.34196 66.71018)", +"http://www.yso.fi/onto/yso/p160956":"POINT(25.624 66.69927)", +"http://www.yso.fi/onto/yso/p161141":"POINT(24.13898 61.82485)", +"http://www.yso.fi/onto/yso/p161196":"POINT(22.12729 60.96766)", +"http://www.yso.fi/onto/yso/p161302":"POINT(22.7937 63.61809)", +"http://www.yso.fi/onto/yso/p161303":"POINT(23.81993 61.47731)", +"http://www.yso.fi/onto/yso/p161304":"POINT(103 30)", +"http://www.yso.fi/onto/yso/p161305":"POINT(23.55887 61.45301)", +"http://www.yso.fi/onto/yso/p161306":"POINT(23.03678 61.79452)", +"http://www.yso.fi/onto/yso/p161307":"POINT(23.16886 61.73061)", +"http://www.yso.fi/onto/yso/p161308":"POINT(23.12452 61.80626)", +"http://www.yso.fi/onto/yso/p161310":"POINT(25.40699 63.21184)", +"http://www.yso.fi/onto/yso/p161311":"POINT(25.40516 63.17468)", +"http://www.yso.fi/onto/yso/p161312":"POINT(22.70206 61.49063)", +"http://www.yso.fi/onto/yso/p161314":"POINT(28.35944444 60.85277778)", +"http://www.yso.fi/onto/yso/p161315":"POINT(25.5605 61.01213)", +"http://www.yso.fi/onto/yso/p161316":"POINT(29.771111111111 61.408333333333)", +"http://www.yso.fi/onto/yso/p161317":"POINT(27.03237 62.01657)", +"http://www.yso.fi/onto/yso/p161329":"POINT(28.8253 65.82908)", +"http://www.yso.fi/onto/yso/p161374":"POINT(25.7703 61.00287)", +"http://www.yso.fi/onto/yso/p161434":"POINT(24.71203 60.32682)", +"http://www.yso.fi/onto/yso/p161556":"", +"http://www.yso.fi/onto/yso/p162248":"POINT(24.20838 63.79598)", +"http://www.yso.fi/onto/yso/p162351":"POINT(24.93145 60.89365)", +"http://www.yso.fi/onto/yso/p162356":"POINT(22.05883 60.78006)", +"http://www.yso.fi/onto/yso/p162358":"POINT(6.6069444444444 52.916666666667)", +"http://www.yso.fi/onto/yso/p162359":"POINT(9.907778 52.757778)", +"http://www.yso.fi/onto/yso/p162361":"POINT(20.86457 63.01616)", +"http://www.yso.fi/onto/yso/p162363":"POINT(25.08187 64.05263)", +"http://www.yso.fi/onto/yso/p162366":"", +"http://www.yso.fi/onto/yso/p162366":"", +"http://www.yso.fi/onto/yso/p162367":"POINT(28.79582 61.217)", +"http://www.yso.fi/onto/yso/p162368":"", +"http://www.yso.fi/onto/yso/p162369":"", +"http://www.yso.fi/onto/yso/p162370":"POINT(22.20001 63.17523)", +"http://www.yso.fi/onto/yso/p162371":"POINT(23.41758 64.06261)", +"http://www.yso.fi/onto/yso/p162374":"POINT(18.425 -33.925)", +"http://www.yso.fi/onto/yso/p162375":"POINT(25.6 -33.958055555556)", +"http://www.yso.fi/onto/yso/p162376":"POINT(18.85 -33.916666666667)", +"http://www.yso.fi/onto/yso/p162377":"POINT(24.38758 60.83581)", +"http://www.yso.fi/onto/yso/p162379":"POINT(46.416388888889 -9.4163888888889)", +"http://www.yso.fi/onto/yso/p162380":"POINT(25.03077 60.19417)", +"http://www.yso.fi/onto/yso/p162381":"POINT(22.94286 62.15168)", +"http://www.yso.fi/onto/yso/p162382":"POINT(22.73719 62.69123)", +"http://www.yso.fi/onto/yso/p162515":"POINT(23.74215 63.26816)", +"http://www.yso.fi/onto/yso/p162549":"POINT(27.03159 60.75583)", +"http://www.yso.fi/onto/yso/p162679":"POINT(24.01041 60.27143)", +"http://www.yso.fi/onto/yso/p162713":"POINT(26.533333333333 -33.3)", +"http://www.yso.fi/onto/yso/p162714":"POINT(31.025 -29.858333333333)", +"http://www.yso.fi/onto/yso/p162826":"POINT(33.833333333333 29.5)", +"http://www.yso.fi/onto/yso/p162827":"POINT(29.18318 61.81776)", +"http://www.yso.fi/onto/yso/p162828":"POINT(27.39941 68.39181)", +"http://www.yso.fi/onto/yso/p162829":"POINT(25.02591 60.14122)", +"http://www.yso.fi/onto/yso/p162831":"POINT(22.52207 63.55259)", +"http://www.yso.fi/onto/yso/p162832":"POINT(22.99024 60.48606)", +"http://www.yso.fi/onto/yso/p162833":"POINT(25.47866 62.99862)", +"http://www.yso.fi/onto/yso/p162835":"POINT(22.92823 61.28741)", +"http://www.yso.fi/onto/yso/p162836":"POINT(24.85845 60.2712)", +"http://www.yso.fi/onto/yso/p162838":"POINT(24.26092 61.33743)", +"http://www.yso.fi/onto/yso/p162839":"POINT(23.63252 60.87932)", +"http://www.yso.fi/onto/yso/p162841":"POINT(29.833333333333 -6.5)", +"http://www.yso.fi/onto/yso/p162842":"POINT(25.63134 61.27172)", +"http://www.yso.fi/onto/yso/p162942":"POINT(8.2666666666667 50)", +"http://www.yso.fi/onto/yso/p163146":"POINT(30.04076 61.84787)", +"http://www.yso.fi/onto/yso/p163168":"POINT(27.04686 62.31559)", +"http://www.yso.fi/onto/yso/p163320":"POINT(22.72893 60.16385)", +"http://www.yso.fi/onto/yso/p163321":"POINT(22.3 60.3)", +"http://www.yso.fi/onto/yso/p163323":"POINT(21.08699 60.13904)", +"http://www.yso.fi/onto/yso/p163326":"POINT(24.5531 62.03695)", +"http://www.yso.fi/onto/yso/p163327":"POINT(23.43623 59.97455)", +"http://www.yso.fi/onto/yso/p163329":"POINT(25.86378 64.27203)", +"http://www.yso.fi/onto/yso/p163354":"", +"http://www.yso.fi/onto/yso/p163355":"", +"http://www.yso.fi/onto/yso/p163356":"POINT(28.185277777778 59.513333333333)", +"http://www.yso.fi/onto/yso/p163357":"POINT(14.3 51.983055555556)", +"http://www.yso.fi/onto/yso/p163358":"POINT(24.7421 60.18669)", +"http://www.yso.fi/onto/yso/p163359":"POINT(29.333333333333 57.033333333333)", +"http://www.yso.fi/onto/yso/p163360":"", +"http://www.yso.fi/onto/yso/p163361":"", +"http://www.yso.fi/onto/yso/p163362":"", +"http://www.yso.fi/onto/yso/p163364":"POINT(21.31174 62.03657)", +"http://www.yso.fi/onto/yso/p163365":"POINT(21.31174 62.03657)", +"http://www.yso.fi/onto/yso/p163367":"POINT(24.34092 61.1179)", +"http://www.yso.fi/onto/yso/p163368":"POINT(27.13285 63.79962)", +"http://www.yso.fi/onto/yso/p163372":"POINT(23.79213 61.14142)", +"http://www.yso.fi/onto/yso/p163517":"POINT(24.94 60.168888888889)", +"http://www.yso.fi/onto/yso/p163740":"", +"http://www.yso.fi/onto/yso/p163741":"POINT(26.02875 60.9243)", +"http://www.yso.fi/onto/yso/p163743":"POINT(24.68448 60.1192)", +"http://www.yso.fi/onto/yso/p163744":"POINT(14.916666666667 55.125)", +"http://www.yso.fi/onto/yso/p163745":"POINT(6.9858333333333 49.811666666667)", +"http://www.yso.fi/onto/yso/p163746":"POINT(6.9663888888889 49.788333333333)", +"http://www.yso.fi/onto/yso/p163747":"POINT(25.61038 63.99457)", +"http://www.yso.fi/onto/yso/p163748":"POINT(25.61713 63.99155)", +"http://www.yso.fi/onto/yso/p163750":"POINT(-63 46.25)", +"http://www.yso.fi/onto/yso/p163751":"POINT(36.266666666667 54.533333333333)", +"http://www.yso.fi/onto/yso/p163752":"POINT(36.616666666667 55.1)", +"http://www.yso.fi/onto/yso/p163753":"POINT(24.78764 61.90869)", +"http://www.yso.fi/onto/yso/p163754":"POINT(24.78764 61.90869)", +"http://www.yso.fi/onto/yso/p163860":"POINT(25.68579 65.33798)", +"http://www.yso.fi/onto/yso/p163876":"POINT(30.604167 59.8075)", +"http://www.yso.fi/onto/yso/p164097":"POINT(22.14123 60.37097)", +"http://www.yso.fi/onto/yso/p164551":"POINT(29.772222222222 60.263888888889)", +"http://www.yso.fi/onto/yso/p164552":"POINT(29.825 60.2725)", +"http://www.yso.fi/onto/yso/p164553":"", +"http://www.yso.fi/onto/yso/p164554":"", +"http://www.yso.fi/onto/yso/p164555":"POINT(29.8 60.3)", +"http://www.yso.fi/onto/yso/p164556":"", +"http://www.yso.fi/onto/yso/p164557":"", +"http://www.yso.fi/onto/yso/p164559":"POINT(27.41374 62.53825)", +"http://www.yso.fi/onto/yso/p164560":"POINT(22.26196 62.80887)", +"http://www.yso.fi/onto/yso/p164561":"POINT(22.22561 62.85095)", +"http://www.yso.fi/onto/yso/p164562":"POINT(22.1833 62.86707)", +"http://www.yso.fi/onto/yso/p164563":"POINT(26.86578 64.28299)", +"http://www.yso.fi/onto/yso/p164566":"POINT(22.27192 63.25507)", +"http://www.yso.fi/onto/yso/p164567":"POINT(21.91572 60.45022)", +"http://www.yso.fi/onto/yso/p164568":"POINT(10.4 43.716666666667)", +"http://www.yso.fi/onto/yso/p164569":"POINT(21.8803 61.14215)", +"http://www.yso.fi/onto/yso/p164570":"POINT(21.95601 61.13482)", +"http://www.yso.fi/onto/yso/p164571":"POINT(-6.1166666666667 36.7)", +"http://www.yso.fi/onto/yso/p164572":"POINT(27.17479 60.56806)", +"http://www.yso.fi/onto/yso/p164573":"POINT(23.19799 59.84452)", +"http://www.yso.fi/onto/yso/p164574":"", +"http://www.yso.fi/onto/yso/p164575":"POINT(28.75 60.65)", +"http://www.yso.fi/onto/yso/p164576":"POINT(25.9405 63.62624)", +"http://www.yso.fi/onto/yso/p164577":"POINT(25.9521 63.66019)", +"http://www.yso.fi/onto/yso/p164591":"POINT(27.1633 63.59114)", +"http://www.yso.fi/onto/yso/p164659":"POINT(-9.25 53.9)", +"http://www.yso.fi/onto/yso/p164682":"POINT(22.64509 61.22933)", +"http://www.yso.fi/onto/yso/p164775":"POINT(23.72813 61.31245)", +"http://www.yso.fi/onto/yso/p164858":"POINT(26.74668 60.90902)", +"http://www.yso.fi/onto/yso/p164859":"POINT(26.699 60.92115)", +"http://www.yso.fi/onto/yso/p164913":"POINT(22.2773 60.46394)", +"http://www.yso.fi/onto/yso/p164949":"POINT(44.05 9.55)", +"http://www.yso.fi/onto/yso/p164975":"POINT(9.9333333333333 56.033333333333)", +"http://www.yso.fi/onto/yso/p165157":"POINT(28.13722 61.05544)", +"http://www.yso.fi/onto/yso/p165158":"POINT(27.97705 60.92908)", +"http://www.yso.fi/onto/yso/p165159":"POINT(27.97705 60.92908)", +"http://www.yso.fi/onto/yso/p165165":"", +"http://www.yso.fi/onto/yso/p165168":"POINT(60.583333333333 56.833333333333)", +"http://www.yso.fi/onto/yso/p165169":"POINT(25.45937 64.98739)", +"http://www.yso.fi/onto/yso/p165170":"POINT(26.90855 60.52715)", +"http://www.yso.fi/onto/yso/p165173":"POINT(25.39571 63.69436)", +"http://www.yso.fi/onto/yso/p165175":"POINT(23.11983 63.62384)", +"http://www.yso.fi/onto/yso/p165176":"POINT(25.9663 61.10778)", +"http://www.yso.fi/onto/yso/p165179":"POINT(25.97676 61.13071)", +"http://www.yso.fi/onto/yso/p165180":"POINT(70.166666666667 62.25)", +"http://www.yso.fi/onto/yso/p165181":"POINT(23.07421 60.63707)", +"http://www.yso.fi/onto/yso/p165220":"POINT(24.3111 64.41253)", +"http://www.yso.fi/onto/yso/p165250":"", +"http://www.yso.fi/onto/yso/p165250":"", +"http://www.yso.fi/onto/yso/p165395":"POINT(23.37939 67.20777)", +"http://www.yso.fi/onto/yso/p165483":"POINT(18.233333333333 59.516666666667)", +"http://www.yso.fi/onto/yso/p165484":"POINT(17.916666666667 59.516666666667)", +"http://www.yso.fi/onto/yso/p165610":"POINT(16.666666666667 56.733333333333)", +"http://www.yso.fi/onto/yso/p165671":"POINT(29.733333 62.416667)", +"http://www.yso.fi/onto/yso/p165672":"", +"http://www.yso.fi/onto/yso/p165673":"POINT(10.209722222222 56.156388888889)", +"http://www.yso.fi/onto/yso/p165674":"POINT(-7 54.4)", +"http://www.yso.fi/onto/yso/p165675":"POINT(24.27193 60.52453)", +"http://www.yso.fi/onto/yso/p165678":"POINT(11.566666666667 48.133333333333)", +"http://www.yso.fi/onto/yso/p165679":"POINT(18.216666666667 46.083333333333)", +"http://www.yso.fi/onto/yso/p165680":"POINT(22.20141 62.72359)", +"http://www.yso.fi/onto/yso/p165681":"POINT(23.26283 61.53788)", +"http://www.yso.fi/onto/yso/p165682":"POINT(23.45943 62.80519)", +"http://www.yso.fi/onto/yso/p165683":"POINT(-82.5 40.5)", +"http://www.yso.fi/onto/yso/p165685":"POINT(23.76939 61.49875)", +"http://www.yso.fi/onto/yso/p165699":"POINT(21.5 65.333333333333)", +"http://www.yso.fi/onto/yso/p165753":"POINT(19.401855555556 69.15645)", +"http://www.yso.fi/onto/yso/p165754":"POINT(14.666666666667 66.833333333333)", +"http://www.yso.fi/onto/yso/p166100":"POINT(16.516666666667 59.366666666667)", +"http://www.yso.fi/onto/yso/p166133":"POINT(25.06843 60.23176)", +"http://www.yso.fi/onto/yso/p166152":"POINT(-4 54)", +"http://www.yso.fi/onto/yso/p166184":"POINT(21.513215 61.128269)", +"http://www.yso.fi/onto/yso/p166195":"POINT(-120.5 47.5)", +"http://www.yso.fi/onto/yso/p166275":"POINT(24.98087 60.23348)", +"http://www.yso.fi/onto/yso/p166344":"POINT(25.01917 60.13527)", +"http://www.yso.fi/onto/yso/p166460":"POINT(12.5685 44.0595)", +"http://www.yso.fi/onto/yso/p166464":"POINT(23.66185 60.50774)", +"http://www.yso.fi/onto/yso/p166466":"POINT(21.52417 61.51792)", +"http://www.yso.fi/onto/yso/p166471":"POINT(21.6038 61.49285)", +"http://www.yso.fi/onto/yso/p166477":"POINT(20.8213 69.01721)", +"http://www.yso.fi/onto/yso/p166479":"POINT(26.28918 65.44059)", +"http://www.yso.fi/onto/yso/p166480":"", +"http://www.yso.fi/onto/yso/p166481":"POINT(23.86584 59.97144)", +"http://www.yso.fi/onto/yso/p166482":"POINT(24.98334 60.27815)", +"http://www.yso.fi/onto/yso/p166483":"POINT(78.5974 30.1451)", +"http://www.yso.fi/onto/yso/p166484":"POINT(91.746833333333 26.200722222222)", +"http://www.yso.fi/onto/yso/p166489":"POINT(9.9294444444444 49.794444444444)", +"http://www.yso.fi/onto/yso/p166490":"POINT(6.6413888888889 49.756666666667)", +"http://www.yso.fi/onto/yso/p166491":"POINT(-7.9072222222222 38.5725)", +"http://www.yso.fi/onto/yso/p166492":"POINT(1.0775 51.278333333333)", +"http://www.yso.fi/onto/yso/p166496":"POINT(12.199444444444 44.417777777778)", +"http://www.yso.fi/onto/yso/p166499":"POINT(-89.3563 48.5629)", +"http://www.yso.fi/onto/yso/p166500":"POINT(29.642777777778 69.972777777778)", +"http://www.yso.fi/onto/yso/p166502":"POINT(28.18428 62.17469)", +"http://www.yso.fi/onto/yso/p166541":"POINT(13.733333333333 51.033333333333)", +"http://www.yso.fi/onto/yso/p166759":"POINT(21.37474 59.77895)", +"http://www.yso.fi/onto/yso/p166760":"POINT(-123.88555555556 46.980277777778)", +"http://www.yso.fi/onto/yso/p166820":"POINT(23.166666666667 65.85)", +"http://www.yso.fi/onto/yso/p166824":"POINT(18.416667 63)", +"http://www.yso.fi/onto/yso/p166825":"POINT(21.296 63.278)", +"http://www.yso.fi/onto/yso/p166876":"", +"http://www.yso.fi/onto/yso/p167590":"POINT(28.25997 62.03988)", +"http://www.yso.fi/onto/yso/p167609":"POINT(22.4311 62.64248)", +"http://www.yso.fi/onto/yso/p167655":"POINT(25.37085 63.9574)", +"http://www.yso.fi/onto/yso/p167657":"POINT(24.71624 60.23053)", +"http://www.yso.fi/onto/yso/p167658":"POINT(24.73178 60.23408)", +"http://www.yso.fi/onto/yso/p167659":"POINT(27.63086 62.16349)", +"http://www.yso.fi/onto/yso/p167661":"POINT(40.45 60.083333333333)", +"http://www.yso.fi/onto/yso/p167667":"POINT(25.15447 61.53786)", +"http://www.yso.fi/onto/yso/p167668":"POINT(25.27754 62.0156)", +"http://www.yso.fi/onto/yso/p167669":"POINT(24.346 60.52118)", +"http://www.yso.fi/onto/yso/p167673":"POINT(51.416666666667 35.7)", +"http://www.yso.fi/onto/yso/p167676":"POINT(23.51224 63.5706)", +"http://www.yso.fi/onto/yso/p167699":"POINT(27.297034 63.119594)", +"http://www.yso.fi/onto/yso/p167703":"POINT(28.41309 66.81864)", +"http://www.yso.fi/onto/yso/p167802":"", +"http://www.yso.fi/onto/yso/p167803":"POINT(43.966666666667 42.233333333333)", +"http://www.yso.fi/onto/yso/p167804":"POINT(44.233333333333 43.183333333333)", +"http://www.yso.fi/onto/yso/p167805":"POINT(40.983333 43)", +"http://www.yso.fi/onto/yso/p167806":"POINT(43.4 43.583333333333)", +"http://www.yso.fi/onto/yso/p167868":"POINT(13.2 55.7)", +"http://www.yso.fi/onto/yso/p167895":"POINT(24.86069 61.60179)", +"http://www.yso.fi/onto/yso/p167902":"POINT(25.01213 60.17074)", +"http://www.yso.fi/onto/yso/p168000":"POINT(21.0161 60.84473)", +"http://www.yso.fi/onto/yso/p168014":"POINT(25.26301 67.28904)", +"http://www.yso.fi/onto/yso/p168103":"POINT(22.34072 60.41136)", +"http://www.yso.fi/onto/yso/p168104":"POINT(25.33002 62.69602)", +"http://www.yso.fi/onto/yso/p168105":"POINT(22.19338 63.28582)", +"http://www.yso.fi/onto/yso/p168106":"POINT(22.80173 62.91595)", +"http://www.yso.fi/onto/yso/p168107":"POINT(22.80173 62.91595)", +"http://www.yso.fi/onto/yso/p168109":"POINT(27.22841 61.96468)", +"http://www.yso.fi/onto/yso/p168110":"POINT(27.22841 61.96468)", +"http://www.yso.fi/onto/yso/p168111":"POINT(27.19911 61.99368)", +"http://www.yso.fi/onto/yso/p168208":"POINT(21.58178 60.24227)", +"http://www.yso.fi/onto/yso/p168283":"POINT(26.60151 60.2771)", +"http://www.yso.fi/onto/yso/p168421":"POINT(23.04848 59.95829)", +"http://www.yso.fi/onto/yso/p168422":"POINT(23.04848 59.95829)", +"http://www.yso.fi/onto/yso/p168591":"POINT(29.7195 40.429)", +"http://www.yso.fi/onto/yso/p168785":"POINT(28.07131 63.79251)", +"http://www.yso.fi/onto/yso/p168987":"POINT(22.28578 63.00281)", +"http://www.yso.fi/onto/yso/p168989":"POINT(28.572222222222 60.770833333333)", +"http://www.yso.fi/onto/yso/p168991":"POINT(28.34399 60.89383)", +"http://www.yso.fi/onto/yso/p168998":"POINT(21.516666666667 47.666666666667)", +"http://www.yso.fi/onto/yso/p169002":"POINT(23.58578 63.09706)", +"http://www.yso.fi/onto/yso/p169003":"POINT(22.98939 61.05129)", +"http://www.yso.fi/onto/yso/p169005":"POINT(24.90686 61.43833)", +"http://www.yso.fi/onto/yso/p169006":"POINT(24.534 60.78437)", +"http://www.yso.fi/onto/yso/p169007":"POINT(-6.2833333333333 36.516666666667)", +"http://www.yso.fi/onto/yso/p169009":"POINT(28.12533 63.39688)", +"http://www.yso.fi/onto/yso/p169014":"POINT(4.5 51)", +"http://www.yso.fi/onto/yso/p169016":"POINT(7.7522222222222 48.573333333333)", +"http://www.yso.fi/onto/yso/p169018":"POINT(18.633333333333 54.366666666667)", +"http://www.yso.fi/onto/yso/p169020":"POINT(7.5 48.5)", +"http://www.yso.fi/onto/yso/p169021":"POINT(7.5 48.5)", +"http://www.yso.fi/onto/yso/p169022":"", +"http://www.yso.fi/onto/yso/p169578":"POINT(37.738 55.628)", +"http://www.yso.fi/onto/yso/p169606":"POINT(107.91666666667 -2.9333333333333)", +"http://www.yso.fi/onto/yso/p169607":"POINT(128 -2)", +"http://www.yso.fi/onto/yso/p169608":"POINT(128 -2)", +"http://www.yso.fi/onto/yso/p169609":"POINT(110 -2)", +"http://www.yso.fi/onto/yso/p169881":"POINT(23.19622 63.81661)", +"http://www.yso.fi/onto/yso/p169883":"POINT(25.44783 65.0471)", +"http://www.yso.fi/onto/yso/p169884":"POINT(25.17342 64.17144)", +"http://www.yso.fi/onto/yso/p169885":"POINT(27.24231 63.3423)", +"http://www.yso.fi/onto/yso/p169889":"POINT(-160.78966666667 63.878908333333)", +"http://www.yso.fi/onto/yso/p169891":"POINT(27.01162 62.23377)", +"http://www.yso.fi/onto/yso/p169892":"POINT(21.78065 61.447)", +"http://www.yso.fi/onto/yso/p169893":"POINT(25.84703 60.6163)", +"http://www.yso.fi/onto/yso/p169894":"POINT(22.11154 63.0669)", +"http://www.yso.fi/onto/yso/p169895":"POINT(26.11924 60.60363)", +"http://www.yso.fi/onto/yso/p169897":"POINT(24.6843 60.20141)", +"http://www.yso.fi/onto/yso/p169900":"", +"http://www.yso.fi/onto/yso/p169901":"POINT(23.54959 60.63634)", +"http://www.yso.fi/onto/yso/p169994":"POINT(74.666666666667 67.25)", +"http://www.yso.fi/onto/yso/p170391":"POINT(22.18283 59.92672)", +"http://www.yso.fi/onto/yso/p170635":"POINT(25.39354 60.59226)", +"http://www.yso.fi/onto/yso/p170636":"POINT(25.55327 60.6954)", +"http://www.yso.fi/onto/yso/p170637":"POINT(24.37907 61.22145)", +"http://www.yso.fi/onto/yso/p170638":"POINT(6.8688888888889 45.922222222222)", +"http://www.yso.fi/onto/yso/p170639":"POINT(22.47154 62.94767)", +"http://www.yso.fi/onto/yso/p170640":"POINT(22.99583 61.96024)", +"http://www.yso.fi/onto/yso/p170642":"POINT(33.021944444444 68.883055555556)", +"http://www.yso.fi/onto/yso/p170719":"POINT(24.87209 60.42513)", +"http://www.yso.fi/onto/yso/p170826":"POINT(24.51766 60.91044)", +"http://www.yso.fi/onto/yso/p170962":"POINT(25.05359 60.1767)", +"http://www.yso.fi/onto/yso/p170963":"POINT(22.05374 62.94653)", +"http://www.yso.fi/onto/yso/p171095":"POINT(26.61671 60.91588)", +"http://www.yso.fi/onto/yso/p171096":"POINT(29.5669 64.13344)", +"http://www.yso.fi/onto/yso/p171098":"POINT(23.82794 61.04575)", +"http://www.yso.fi/onto/yso/p171100":"POINT(26.57527 60.899)", +"http://www.yso.fi/onto/yso/p171103":"POINT(22.92169 60.6029)", +"http://www.yso.fi/onto/yso/p171104":"POINT(27.18485 63.08511)", +"http://www.yso.fi/onto/yso/p171105":"POINT(27.08311 63.10996)", +"http://www.yso.fi/onto/yso/p171106":"POINT(25.71191 60.39167)", +"http://www.yso.fi/onto/yso/p171155":"POINT(14.333333333333 58.05)", +"http://www.yso.fi/onto/yso/p171177":"POINT(24.99493 60.20079)", +"http://www.yso.fi/onto/yso/p171212":"POINT(23.72344 60.6996)", +"http://www.yso.fi/onto/yso/p171360":"POINT(21.95914 63.06495)", +"http://www.yso.fi/onto/yso/p172104":"", +"http://www.yso.fi/onto/yso/p172163":"POINT(15.436269444444 60.485555555556)", +"http://www.yso.fi/onto/yso/p172165":"POINT(23.536944444444 58.946944444444)", +"http://www.yso.fi/onto/yso/p172166":"POINT(23.25 59)", +"http://www.yso.fi/onto/yso/p172167":"POINT(24.394 60.48719)", +"http://www.yso.fi/onto/yso/p172168":"POINT(29.23393 62.43411)", +"http://www.yso.fi/onto/yso/p172169":"POINT(24.06038 60.26243)", +"http://www.yso.fi/onto/yso/p172170":"POINT(24.915 60.16)", +"http://www.yso.fi/onto/yso/p172172":"POINT(83.006111111111 25.306944444444)", +"http://www.yso.fi/onto/yso/p172173":"POINT(25.39656 62.16534)", +"http://www.yso.fi/onto/yso/p172174":"POINT(27.56172 60.92106)", +"http://www.yso.fi/onto/yso/p172186":"POINT(24.18833 60.54267)", +"http://www.yso.fi/onto/yso/p172271":"POINT(32.610277777778 25.720555555556)", +"http://www.yso.fi/onto/yso/p172272":"POINT(32.642222222222 25.696944444444)", +"http://www.yso.fi/onto/yso/p172322":"POINT(22.14558 62.57877)", +"http://www.yso.fi/onto/yso/p172328":"POINT(27.29273 67.254)", +"http://www.yso.fi/onto/yso/p172714":"POINT(14.666666666667 63)", +"http://www.yso.fi/onto/yso/p172715":"POINT(26.88791 69.35517)", +"http://www.yso.fi/onto/yso/p172716":"POINT(-96.414166666667 16.898055555556)", +"http://www.yso.fi/onto/yso/p172717":"POINT(25.67399 61.75221)", +"http://www.yso.fi/onto/yso/p172718":"POINT(24.22679 60.99822)", +"http://www.yso.fi/onto/yso/p172719":"POINT(25.76101 62.24058)", +"http://www.yso.fi/onto/yso/p172720":"POINT(23.69586 67.95479)", +"http://www.yso.fi/onto/yso/p172721":"POINT(142 43)", +"http://www.yso.fi/onto/yso/p172722":"POINT(150 55)", +"http://www.yso.fi/onto/yso/p172725":"POINT(25.14876 61.9801)", +"http://www.yso.fi/onto/yso/p173376":"POINT(13.1 59.35)", +"http://www.yso.fi/onto/yso/p173404":"POINT(21.5688 61.56793)", +"http://www.yso.fi/onto/yso/p173436":"", +"http://www.yso.fi/onto/yso/p173475":"POINT(24.38502 61.25899)", +"http://www.yso.fi/onto/yso/p173476":"POINT(24.69537 61.28688)", +"http://www.yso.fi/onto/yso/p173477":"POINT(24.69537 61.28688)", +"http://www.yso.fi/onto/yso/p173478":"POINT(26.87382 62.6196)", +"http://www.yso.fi/onto/yso/p173479":"POINT(25.55746 63.20249)", +"http://www.yso.fi/onto/yso/p173480":"POINT(25.66669 63.23453)", +"http://www.yso.fi/onto/yso/p173483":"POINT(25.6586 61.02236)", +"http://www.yso.fi/onto/yso/p173484":"POINT(21.55788 63.1461)", +"http://www.yso.fi/onto/yso/p173485":"POINT(27.28939 61.91187)", +"http://www.yso.fi/onto/yso/p173486":"POINT(29.11554 62.66044)", +"http://www.yso.fi/onto/yso/p173487":"POINT(23.21663 60.79914)", +"http://www.yso.fi/onto/yso/p173488":"POINT(23.17284 60.78499)", +"http://www.yso.fi/onto/yso/p173489":"POINT(23.21641 60.79772)", +"http://www.yso.fi/onto/yso/p173490":"", +"http://www.yso.fi/onto/yso/p173491":"POINT(23.43744 61.55034)", +"http://www.yso.fi/onto/yso/p173492":"POINT(23.43744 61.55034)", +"http://www.yso.fi/onto/yso/p173509":"POINT(26.841944 37.146667)", +"http://www.yso.fi/onto/yso/p173872":"POINT(34.333333333333 31.416666666667)", +"http://www.yso.fi/onto/yso/p173876":"POINT(36.366666666667 0.033333333333333)", +"http://www.yso.fi/onto/yso/p173880":"POINT(34.45 31.516666666667)", +"http://www.yso.fi/onto/yso/p173882":"POINT(5.45279 51.45037)", +"http://www.yso.fi/onto/yso/p173939":"POINT(24.60538 61.34957)", +"http://www.yso.fi/onto/yso/p173940":"POINT(24.60538 61.34957)", +"http://www.yso.fi/onto/yso/p173943":"POINT(16.93 62.739722222222)", +"http://www.yso.fi/onto/yso/p173967":"POINT(36.291944444444 33.513055555556)", +"http://www.yso.fi/onto/yso/p174367":"POINT(22.38265 63.48726)", +"http://www.yso.fi/onto/yso/p174368":"", +"http://www.yso.fi/onto/yso/p174369":"POINT(25.77307 65.1297)", +"http://www.yso.fi/onto/yso/p174370":"", +"http://www.yso.fi/onto/yso/p174374":"POINT(85.5 20.15)", +"http://www.yso.fi/onto/yso/p174375":"POINT(24.44353 60.99417)", +"http://www.yso.fi/onto/yso/p174376":"POINT(23.6308 63.21343)", +"http://www.yso.fi/onto/yso/p174377":"POINT(22.69388 62.06649)", +"http://www.yso.fi/onto/yso/p174378":"POINT(22.71977 62.11767)", +"http://www.yso.fi/onto/yso/p174379":"POINT(25.16073 60.30941)", +"http://www.yso.fi/onto/yso/p174380":"POINT(24.88238 67.35492)", +"http://www.yso.fi/onto/yso/p174405":"POINT(22.29615 61.25408)", +"http://www.yso.fi/onto/yso/p174444":"POINT(24.50241 64.67955)", +"http://www.yso.fi/onto/yso/p174462":"POINT(22.92777 60.78)", +"http://www.yso.fi/onto/yso/p174481":"POINT(24.70927 64.14448)", +"http://www.yso.fi/onto/yso/p174543":"POINT(25.17452 60.25489)", +"http://www.yso.fi/onto/yso/p174740":"", +"http://www.yso.fi/onto/yso/p174743":"POINT(25.00294 61.91342)", +"http://www.yso.fi/onto/yso/p174744":"POINT(30.525 60.155555555556)", +"http://www.yso.fi/onto/yso/p174745":"POINT(23.52214 60.2107)", +"http://www.yso.fi/onto/yso/p174746":"", +"http://www.yso.fi/onto/yso/p174747":"POINT(26.12606 62.23916)", +"http://www.yso.fi/onto/yso/p174748":"POINT(27.95692 64.09624)", +"http://www.yso.fi/onto/yso/p174821":"POINT(7.9166666666667 45.25)", +"http://www.yso.fi/onto/yso/p175022":"POINT(15.416666666667 60.133333333333)", +"http://www.yso.fi/onto/yso/p175046":"POINT(27.60685 63.14748)", +"http://www.yso.fi/onto/yso/p175114":"POINT(87.61379 43.8253)", +"http://www.yso.fi/onto/yso/p175115":"POINT(79.970833333333 30.540555555556)", +"http://www.yso.fi/onto/yso/p175135":"POINT(22.88509 61.31311)", +"http://www.yso.fi/onto/yso/p175533":"POINT(29.6 46.816667)", +"http://www.yso.fi/onto/yso/p175558":"POINT(-6.15 39.2)", +"http://www.yso.fi/onto/yso/p175560":"POINT(23.66263 60.26946)", +"http://www.yso.fi/onto/yso/p175562":"POINT(25.4759 66.42408)", +"http://www.yso.fi/onto/yso/p175563":"POINT(25.58611 60.44963)", +"http://www.yso.fi/onto/yso/p175565":"POINT(24.01777 64.23226)", +"http://www.yso.fi/onto/yso/p176085":"POINT(30.644444 66.163333)", +"http://www.yso.fi/onto/yso/p176119":"POINT(25.86028 60.2208)", +"http://www.yso.fi/onto/yso/p176182":"POINT(25.64086 60.36352)", +"http://www.yso.fi/onto/yso/p176245":"", +"http://www.yso.fi/onto/yso/p176246":"", +"http://www.yso.fi/onto/yso/p176318":"POINT(14.352608333333 40.806791666667)", +"http://www.yso.fi/onto/yso/p176319":"POINT(14.373611111111 40.627777777778)", +"http://www.yso.fi/onto/yso/p176320":"POINT(14.602777777778 40.633333333333)", +"http://www.yso.fi/onto/yso/p176321":"POINT(13.910555555556 40.72)", +"http://www.yso.fi/onto/yso/p176322":"POINT(14.233333333333 40.55)", +"http://www.yso.fi/onto/yso/p176359":"POINT(24.42057 66.64514)", +"http://www.yso.fi/onto/yso/p176393":"POINT(21.88362 61.49454)", +"http://www.yso.fi/onto/yso/p176394":"POINT(-149.46666666667 -17.666666666667)", +"http://www.yso.fi/onto/yso/p176395":"POINT(-149.56666666667 -17.533333333333)", +"http://www.yso.fi/onto/yso/p176396":"POINT(21.80753 62.8249)", +"http://www.yso.fi/onto/yso/p176397":"POINT(21.80753 62.8249)", +"http://www.yso.fi/onto/yso/p176820":"POINT(12.383333333333 51.333333333333)", +"http://www.yso.fi/onto/yso/p176822":"POINT(30.25638889 60.31111111)", +"http://www.yso.fi/onto/yso/p176823":"POINT(30.75636 60.417432)", +"http://www.yso.fi/onto/yso/p176824":"POINT(30.5275 60.339166666667)", +"http://www.yso.fi/onto/yso/p176825":"", +"http://www.yso.fi/onto/yso/p176827":"POINT(30.643447 60.034414)", +"http://www.yso.fi/onto/yso/p176830":"POINT(31.166666666667 65.2)", +"http://www.yso.fi/onto/yso/p176831":"POINT(31.166666666667 65.2)", +"http://www.yso.fi/onto/yso/p176835":"POINT(21.98044 63.45667)", +"http://www.yso.fi/onto/yso/p176836":"POINT(20.34788 59.50458)", +"http://www.yso.fi/onto/yso/p176839":"POINT(28.80778313 60.73277664)", +"http://www.yso.fi/onto/yso/p176844":"POINT(24.13459 60.40904)", +"http://www.yso.fi/onto/yso/p176845":"POINT(24.92476 65.68138)", +"http://www.yso.fi/onto/yso/p176846":"POINT(26.73947 65.41901)", +"http://www.yso.fi/onto/yso/p176848":"POINT(25.68528 60.98321)", +"http://www.yso.fi/onto/yso/p176850":"POINT(20.333333333333 39.583333333333)", +"http://www.yso.fi/onto/yso/p176852":"POINT(30.014031 60.487191)", +"http://www.yso.fi/onto/yso/p176853":"", +"http://www.yso.fi/onto/yso/p176855":"", +"http://www.yso.fi/onto/yso/p176858":"POINT(25 24)", +"http://www.yso.fi/onto/yso/p176957":"POINT(26.45532 68.40759)", +"http://www.yso.fi/onto/yso/p176958":"POINT(74.043333 15.401944)", +"http://www.yso.fi/onto/yso/p176980":"POINT(-80.216666666667 25.783333333333)", +"http://www.yso.fi/onto/yso/p176981":"POINT(-81.544166666667 24.666944444444)", +"http://www.yso.fi/onto/yso/p176983":"POINT(-46.61667 -23.5)", +"http://www.yso.fi/onto/yso/p177609":"", +"http://www.yso.fi/onto/yso/p178067":"POINT(22.01666667 52.66666667)", +"http://www.yso.fi/onto/yso/p178090":"POINT(23.1014 61.50488)", +"http://www.yso.fi/onto/yso/p178091":"POINT(23.13258 61.50087)", +"http://www.yso.fi/onto/yso/p178092":"POINT(23.79213 61.14142)", +"http://www.yso.fi/onto/yso/p178093":"", +"http://www.yso.fi/onto/yso/p178095":"POINT(24.44839 60.98859)", +"http://www.yso.fi/onto/yso/p178098":"POINT(25.37185 60.81378)", +"http://www.yso.fi/onto/yso/p178102":"POINT(27.48041 63.25322)", +"http://www.yso.fi/onto/yso/p178105":"POINT(22.28311 60.43347)", +"http://www.yso.fi/onto/yso/p178107":"POINT(26.67808 65.73767)", +"http://www.yso.fi/onto/yso/p178108":"POINT(26.66806 65.71526)", +"http://www.yso.fi/onto/yso/p178116":"", +"http://www.yso.fi/onto/yso/p178116":"", +"http://www.yso.fi/onto/yso/p178118":"POINT(24.48996 63.57398)", +"http://www.yso.fi/onto/yso/p178125":"POINT(4.4947222222222 52.154444444444)", +"http://www.yso.fi/onto/yso/p178205":"POINT(7.2683333333333 43.701944444444)", +"http://www.yso.fi/onto/yso/p178475":"POINT(-122.25 37.8)", +"http://www.yso.fi/onto/yso/p178690":"POINT(28.50283 66.9222)", +"http://www.yso.fi/onto/yso/p178691":"", +"http://www.yso.fi/onto/yso/p178692":"POINT(20.75 37.8)", +"http://www.yso.fi/onto/yso/p178693":"POINT(20.5 38.2)", +"http://www.yso.fi/onto/yso/p178694":"", +"http://www.yso.fi/onto/yso/p178695":"POINT(24.82532 64.07927)", +"http://www.yso.fi/onto/yso/p178696":"POINT(28.006 63.13067)", +"http://www.yso.fi/onto/yso/p178697":"POINT(28.0631 60.83275)", +"http://www.yso.fi/onto/yso/p178698":"POINT(21.49709 62.96963)", +"http://www.yso.fi/onto/yso/p178699":"POINT(22.47014 61.21225)", +"http://www.yso.fi/onto/yso/p178700":"", +"http://www.yso.fi/onto/yso/p178701":"", +"http://www.yso.fi/onto/yso/p178702":"", +"http://www.yso.fi/onto/yso/p178703":"POINT(25.69948 62.12499)", +"http://www.yso.fi/onto/yso/p178704":"", +"http://www.yso.fi/onto/yso/p178705":"POINT(28.81472 61.15491)", +"http://www.yso.fi/onto/yso/p178915":"POINT(30.35663 62.84722)", +"http://www.yso.fi/onto/yso/p178916":"POINT(30.26656 62.8581)", +"http://www.yso.fi/onto/yso/p178917":"POINT(30.42758 62.87598)", +"http://www.yso.fi/onto/yso/p179007":"POINT(-15.58056 27.75)", +"http://www.yso.fi/onto/yso/p179010":"POINT(27.56645 60.7254)", +"http://www.yso.fi/onto/yso/p179356":"POINT(-9.7 53.116666666667)", +"http://www.yso.fi/onto/yso/p179417":"POINT(23.13665 61.41975)", +"http://www.yso.fi/onto/yso/p179464":"POINT(23.76292 61.49968)", +"http://www.yso.fi/onto/yso/p179525":"", +"http://www.yso.fi/onto/yso/p179526":"", +"http://www.yso.fi/onto/yso/p179657":"POINT(49.166666666667 9.8333333333333)", +"http://www.yso.fi/onto/yso/p179658":"POINT(140.47333333333 37.760833333333)", +"http://www.yso.fi/onto/yso/p179659":"POINT(27.262 61.95554)", +"http://www.yso.fi/onto/yso/p179660":"POINT(27.262 61.95554)", +"http://www.yso.fi/onto/yso/p179661":"POINT(21.80579 63.02805)", +"http://www.yso.fi/onto/yso/p179663":"POINT(28.55913 65.73577)", +"http://www.yso.fi/onto/yso/p179664":"POINT(22.52955 62.92026)", +"http://www.yso.fi/onto/yso/p179665":"POINT(27.17737 61.6892)", +"http://www.yso.fi/onto/yso/p179666":"POINT(21.39026 60.80686)", +"http://www.yso.fi/onto/yso/p179667":"POINT(26.92483 60.49638)", +"http://www.yso.fi/onto/yso/p179671":"POINT(26.95698 60.49899)", +"http://www.yso.fi/onto/yso/p179672":"POINT(24.8224 60.79579)", +"http://www.yso.fi/onto/yso/p179673":"POINT(24.85847 60.7984)", +"http://www.yso.fi/onto/yso/p179674":"POINT(24.81505 60.82021)", +"http://www.yso.fi/onto/yso/p179675":"POINT(23.54282 60.12992)", +"http://www.yso.fi/onto/yso/p179676":"POINT(25.04214 60.31056)", +"http://www.yso.fi/onto/yso/p179677":"POINT(25.05678 60.29827)", +"http://www.yso.fi/onto/yso/p179679":"POINT(24.61646 64.58602)", +"http://www.yso.fi/onto/yso/p179680":"POINT(24.61646 64.58602)", +"http://www.yso.fi/onto/yso/p179681":"POINT(24.52077 64.5875)", +"http://www.yso.fi/onto/yso/p179682":"POINT(24.52077 64.5875)", +"http://www.yso.fi/onto/yso/p179683":"POINT(24.67461 64.56299)", +"http://www.yso.fi/onto/yso/p179684":"POINT(24.67461 64.56299)", +"http://www.yso.fi/onto/yso/p179685":"POINT(24.62029 64.62059)", +"http://www.yso.fi/onto/yso/p179686":"POINT(24.62029 64.62059)", +"http://www.yso.fi/onto/yso/p179809":"POINT(-0.408 38.354)", +"http://www.yso.fi/onto/yso/p180122":"POINT(29.13327 61.86018)", +"http://www.yso.fi/onto/yso/p180148":"POINT(23.13592 61.73076)", +"http://www.yso.fi/onto/yso/p180149":"POINT(23.09993 61.75694)", +"http://www.yso.fi/onto/yso/p180150":"POINT(23.40403 61.46351)", +"http://www.yso.fi/onto/yso/p180290":"POINT(19.69952 60.36857)", +"http://www.yso.fi/onto/yso/p180332":"POINT(23.25 58.6)", +"http://www.yso.fi/onto/yso/p180333":"POINT(22.503888888889 58.163888888889)", +"http://www.yso.fi/onto/yso/p180334":"POINT(25.16879 60.55034)", +"http://www.yso.fi/onto/yso/p180386":"POINT(30 7)", +"http://www.yso.fi/onto/yso/p180387":"POINT(12.163056 46.613056)", +"http://www.yso.fi/onto/yso/p180574":"POINT(23.08649 63.34)", +"http://www.yso.fi/onto/yso/p180575":"POINT(23.04157 63.35648)", +"http://www.yso.fi/onto/yso/p180576":"POINT(23.02895 63.32712)", +"http://www.yso.fi/onto/yso/p180578":"POINT(21.715 62.46798)", +"http://www.yso.fi/onto/yso/p180579":"POINT(23.50289 60.63945)", +"http://www.yso.fi/onto/yso/p180581":"", +"http://www.yso.fi/onto/yso/p180582":"", +"http://www.yso.fi/onto/yso/p180584":"POINT(12.3975 42.205555555556)", +"http://www.yso.fi/onto/yso/p180597":"POINT(25.6131 64.71848)", +"http://www.yso.fi/onto/yso/p180607":"POINT(23.78027 61.86145)", +"http://www.yso.fi/onto/yso/p181131":"POINT(25.48063 60.34426)", +"http://www.yso.fi/onto/yso/p181155":"POINT(20.50742 60.28994)", +"http://www.yso.fi/onto/yso/p181500":"POINT(28.89618 62.03274)", +"http://www.yso.fi/onto/yso/p181501":"POINT(28.83052 62.04227)", +"http://www.yso.fi/onto/yso/p181504":"POINT(27.34001 61.59202)", +"http://www.yso.fi/onto/yso/p181505":"POINT(27.47434 61.56893)", +"http://www.yso.fi/onto/yso/p181506":"POINT(27.41473 61.58152)", +"http://www.yso.fi/onto/yso/p181507":"POINT(27.47307 61.53772)", +"http://www.yso.fi/onto/yso/p181508":"POINT(27.46851 61.58559)", +"http://www.yso.fi/onto/yso/p181509":"POINT(27.5116 61.52106)", +"http://www.yso.fi/onto/yso/p181510":"POINT(23.0486 60.13765)", +"http://www.yso.fi/onto/yso/p181511":"POINT(25.70013 62.25701)", +"http://www.yso.fi/onto/yso/p181513":"POINT(25.74485 61.01145)", +"http://www.yso.fi/onto/yso/p181514":"POINT(29.2057 64.36136)", +"http://www.yso.fi/onto/yso/p181515":"POINT(25.00445 60.30036)", +"http://www.yso.fi/onto/yso/p181516":"POINT(24.42165 60.98378)", +"http://www.yso.fi/onto/yso/p181517":"", +"http://www.yso.fi/onto/yso/p181913":"POINT(25.06842 62.59853)", +"http://www.yso.fi/onto/yso/p182121":"POINT(22.27874 61.38317)", +"http://www.yso.fi/onto/yso/p182181":"POINT(28.7965 65.17415)", +"http://www.yso.fi/onto/yso/p182182":"POINT(30.448611111111 60.045833333333)", +"http://www.yso.fi/onto/yso/p182183":"POINT(24.95496 65.04636)", +"http://www.yso.fi/onto/yso/p182185":"POINT(8.5 59.333333333333)", +"http://www.yso.fi/onto/yso/p182187":"POINT(22.75903 68.60223)", +"http://www.yso.fi/onto/yso/p182188":"POINT(26.4 69.1)", +"http://www.yso.fi/onto/yso/p182191":"POINT(23.81614 61.49976)", +"http://www.yso.fi/onto/yso/p182192":"POINT(23.77552 61.46896)", +"http://www.yso.fi/onto/yso/p182193":"POINT(23.76669 61.46813)", +"http://www.yso.fi/onto/yso/p182199":"POINT(23.88625 61.49083)", +"http://www.yso.fi/onto/yso/p182200":"POINT(23.60216 61.5181)", +"http://www.yso.fi/onto/yso/p182201":"POINT(23.70571 61.52734)", +"http://www.yso.fi/onto/yso/p182205":"POINT(23.34331 63.90325)", +"http://www.yso.fi/onto/yso/p182206":"POINT(24.30961 61.60753)", +"http://www.yso.fi/onto/yso/p182207":"POINT(24.32591 61.59653)", +"http://www.yso.fi/onto/yso/p182514":"POINT(36.066666666667 -0.28333333333333)", +"http://www.yso.fi/onto/yso/p182750":"POINT(25.66506 60.97426)", +"http://www.yso.fi/onto/yso/p182824":"POINT(24.12103 61.09155)", +"http://www.yso.fi/onto/yso/p182853":"POINT(10.248055555556 60.023611111111)", +"http://www.yso.fi/onto/yso/p182916":"POINT(49.362777777778 69.019166666667)", +"http://www.yso.fi/onto/yso/p183298":"POINT(22.34987 63.42006)", +"http://www.yso.fi/onto/yso/p183300":"POINT(121 14.583333333333)", +"http://www.yso.fi/onto/yso/p183301":"POINT(22.38824 60.40552)", +"http://www.yso.fi/onto/yso/p183303":"POINT(24.70402 60.13802)", +"http://www.yso.fi/onto/yso/p183304":"POINT(24.70038 60.13607)", +"http://www.yso.fi/onto/yso/p183305":"POINT(24.7346 60.20353)", +"http://www.yso.fi/onto/yso/p183306":"POINT(25.08852 62.46087)", +"http://www.yso.fi/onto/yso/p183310":"POINT(21.71686 61.15658)", +"http://www.yso.fi/onto/yso/p183312":"POINT(27.87321 67.56176)", +"http://www.yso.fi/onto/yso/p183314":"", +"http://www.yso.fi/onto/yso/p183316":"POINT(25.37345 65.32604)", +"http://www.yso.fi/onto/yso/p183317":"POINT(25.45458 65.01817)", +"http://www.yso.fi/onto/yso/p183446":"POINT(23.20793 59.87948)", +"http://www.yso.fi/onto/yso/p183484":"POINT(25.70837 62.28423)", +"http://www.yso.fi/onto/yso/p183910":"POINT(64 31)", +"http://www.yso.fi/onto/yso/p183974":"POINT(29.03754 61.81932)", +"http://www.yso.fi/onto/yso/p183980":"POINT(25.2934 61.3296)", +"http://www.yso.fi/onto/yso/p183981":"POINT(102.6 17.966666666667)", +"http://www.yso.fi/onto/yso/p183982":"POINT(25.68127 67.56172)", +"http://www.yso.fi/onto/yso/p183983":"POINT(25.61665 67.50942)", +"http://www.yso.fi/onto/yso/p183984":"POINT(22.51214 60.01995)", +"http://www.yso.fi/onto/yso/p183985":"POINT(39.21888889 35.28972222)", +"http://www.yso.fi/onto/yso/p183987":"POINT(26.92542 66.13116)", +"http://www.yso.fi/onto/yso/p184482":"POINT(26.9657 60.49058)", +"http://www.yso.fi/onto/yso/p184559":"", +"http://www.yso.fi/onto/yso/p184560":"", +"http://www.yso.fi/onto/yso/p184574":"", +"http://www.yso.fi/onto/yso/p185067":"POINT(24.50454 61.6181)", +"http://www.yso.fi/onto/yso/p185068":"POINT(27.69836 62.90335)", +"http://www.yso.fi/onto/yso/p185095":"POINT(-97.962222222222 17.186111111111)", +"http://www.yso.fi/onto/yso/p185096":"POINT(23.82759 60.77652)", +"http://www.yso.fi/onto/yso/p185101":"POINT(24.83455 61.11833)", +"http://www.yso.fi/onto/yso/p185102":"POINT(-68.73333 -16.35)", +"http://www.yso.fi/onto/yso/p185104":"POINT(24.6525 70.61305556)", +"http://www.yso.fi/onto/yso/p185105":"POINT(25.05772 60.25868)", +"http://www.yso.fi/onto/yso/p185106":"POINT(26.17793 61.08068)", +"http://www.yso.fi/onto/yso/p185110":"POINT(25.29054 60.22837)", +"http://www.yso.fi/onto/yso/p185111":"POINT(28.03074 62.45458)", +"http://www.yso.fi/onto/yso/p185112":"POINT(9.17944 48.7786)", +"http://www.yso.fi/onto/yso/p185115":"POINT(23.75345 61.33741)", +"http://www.yso.fi/onto/yso/p185116":"POINT(23.75876 61.33198)", +"http://www.yso.fi/onto/yso/p185117":"POINT(23.77039 61.33796)", +"http://www.yso.fi/onto/yso/p185118":"POINT(23.76432 61.33463)", +"http://www.yso.fi/onto/yso/p185318":"POINT(131.9 43.116666666667)", +"http://www.yso.fi/onto/yso/p185450":"POINT(43 63.5)", +"http://www.yso.fi/onto/yso/p185975":"POINT(136.83333333333 54.8)", +"http://www.yso.fi/onto/yso/p186110":"POINT(8.6666666666667 50.583333333333)", +"http://www.yso.fi/onto/yso/p186112":"POINT(26.3645 61.26893)", +"http://www.yso.fi/onto/yso/p186113":"POINT(28.83603 61.25643)", +"http://www.yso.fi/onto/yso/p186116":"POINT(28.73062 64.16287)", +"http://www.yso.fi/onto/yso/p186117":"POINT(28.6178 64.15517)", +"http://www.yso.fi/onto/yso/p186118":"POINT(24.51959 60.88088)", +"http://www.yso.fi/onto/yso/p186123":"POINT(25.14515 61.74576)", +"http://www.yso.fi/onto/yso/p186125":"POINT(28.09383 61.02836)", +"http://www.yso.fi/onto/yso/p186126":"POINT(28.09383 61.02836)", +"http://www.yso.fi/onto/yso/p186127":"POINT(-90.05 29.966666666667)", +"http://www.yso.fi/onto/yso/p186133":"POINT(21.7975 61.7469)", +"http://www.yso.fi/onto/yso/p186644":"POINT(106.91666666667 47.916666666667)", +"http://www.yso.fi/onto/yso/p187156":"POINT(23.7608 61.4982)", +"http://www.yso.fi/onto/yso/p187157":"POINT(-3.6008333333333 37.178055555556)", +"http://www.yso.fi/onto/yso/p187158":"POINT(25.14325 60.24916)", +"http://www.yso.fi/onto/yso/p187159":"POINT(24.15 59.216666666667)", +"http://www.yso.fi/onto/yso/p187161":"POINT(17.083611111111 -22.57)", +"http://www.yso.fi/onto/yso/p187162":"POINT(27.71545 61.79442)", +"http://www.yso.fi/onto/yso/p187163":"POINT(25.16328 62.13705)", +"http://www.yso.fi/onto/yso/p187164":"POINT(25.10798 61.19282)", +"http://www.yso.fi/onto/yso/p187165":"POINT(7.2 51.266666666667)", +"http://www.yso.fi/onto/yso/p187166":"POINT(7.149167 51.257222)", +"http://www.yso.fi/onto/yso/p187167":"POINT(6.9566666666667 50.474444444444)", +"http://www.yso.fi/onto/yso/p187168":"POINT(21.53875 61.77331)", +"http://www.yso.fi/onto/yso/p187169":"POINT(23.23161 60.01925)", +"http://www.yso.fi/onto/yso/p187170":"POINT(23.23161 60.01925)", +"http://www.yso.fi/onto/yso/p187171":"POINT(23.95087 62.43487)", +"http://www.yso.fi/onto/yso/p187172":"POINT(29.54284 62.1735)", +"http://www.yso.fi/onto/yso/p187174":"POINT(24.81286 60.27114)", +"http://www.yso.fi/onto/yso/p187175":"POINT(25.12039 66.16029)", +"http://www.yso.fi/onto/yso/p187176":"POINT(26.43464 64.17068)", +"http://www.yso.fi/onto/yso/p187177":"POINT(26.18866 64.10677)", +"http://www.yso.fi/onto/yso/p187180":"POINT(26.18866 64.10677)", +"http://www.yso.fi/onto/yso/p187182":"POINT(21.91102 63.22734)", +"http://www.yso.fi/onto/yso/p187184":"POINT(27.5227 62.08044)", +"http://www.yso.fi/onto/yso/p187185":"POINT(25.15687 60.17123)", +"http://www.yso.fi/onto/yso/p187193":"POINT(22.40107 63.33486)", +"http://www.yso.fi/onto/yso/p187194":"POINT(29.40292 62.31246)", +"http://www.yso.fi/onto/yso/p187195":"POINT(16.512777777778 41.008611111111)", +"http://www.yso.fi/onto/yso/p187196":"POINT(8.7666666666667 44.45)", +"http://www.yso.fi/onto/yso/p187550":"POINT(27.8175 60.487777777778)", +"http://www.yso.fi/onto/yso/p188037":"POINT(24.28799 61.84394)", +"http://www.yso.fi/onto/yso/p188394":"", +"http://www.yso.fi/onto/yso/p188395":"POINT(25.4346 64.76775)", +"http://www.yso.fi/onto/yso/p188396":"POINT(22.04188 62.96601)", +"http://www.yso.fi/onto/yso/p188397":"POINT(22.09148 62.96262)", +"http://www.yso.fi/onto/yso/p188398":"POINT(22.02867 62.95673)", +"http://www.yso.fi/onto/yso/p188399":"POINT(22.02682 62.96711)", +"http://www.yso.fi/onto/yso/p188400":"POINT(34.397777777778 61.786944444444)", +"http://www.yso.fi/onto/yso/p188401":"POINT(-89.23 48.45)", +"http://www.yso.fi/onto/yso/p188402":"POINT(24.11283 64.4089)", +"http://www.yso.fi/onto/yso/p188403":"POINT(24.14973 60.42356)", +"http://www.yso.fi/onto/yso/p188404":"POINT(34.327569444444 56.265583333333)", +"http://www.yso.fi/onto/yso/p188405":"POINT(12.6175 43.07)", +"http://www.yso.fi/onto/yso/p188406":"POINT(12.1 42.716666666667)", +"http://www.yso.fi/onto/yso/p188407":"POINT(39.172777777778 21.542777777778)", +"http://www.yso.fi/onto/yso/p188408":"POINT(25.4826 65.03251)", +"http://www.yso.fi/onto/yso/p188410":"POINT(24.71069 60.15853)", +"http://www.yso.fi/onto/yso/p188427":"POINT(29.44032 63.45822)", +"http://www.yso.fi/onto/yso/p188430":"POINT(22.97901 61.74717)", +"http://www.yso.fi/onto/yso/p188431":"", +"http://www.yso.fi/onto/yso/p188432":"POINT(21.7919 62.05989)", +"http://www.yso.fi/onto/yso/p188433":"POINT(21.84112 62.03639)", +"http://www.yso.fi/onto/yso/p188566":"POINT(22.99897 62.61277)", +"http://www.yso.fi/onto/yso/p188726":"POINT(22.40928 59.92946)", +"http://www.yso.fi/onto/yso/p188858":"POINT(22.113611 61.3375)", +"http://www.yso.fi/onto/yso/p188860":"POINT(33.732592 61.014522)", +"http://www.yso.fi/onto/yso/p188861":"POINT(26.64863 61.29172)", +"http://www.yso.fi/onto/yso/p188862":"POINT(25.88705 61.76987)", +"http://www.yso.fi/onto/yso/p188863":"POINT(6.0825 50.774167)", +"http://www.yso.fi/onto/yso/p188865":"POINT(151.5 46.5)", +"http://www.yso.fi/onto/yso/p188866":"POINT(28.45888 62.97898)", +"http://www.yso.fi/onto/yso/p188867":"POINT(30.165066666667 69.430927777778)", +"http://www.yso.fi/onto/yso/p188868":"POINT(25.09768 61.44866)", +"http://www.yso.fi/onto/yso/p188870":"POINT(24.09217 60.64736)", +"http://www.yso.fi/onto/yso/p188871":"POINT(23.95265 60.49178)", +"http://www.yso.fi/onto/yso/p188872":"POINT(23.95265 60.49178)", +"http://www.yso.fi/onto/yso/p188879":"POINT(26.95598 63.4703)", +"http://www.yso.fi/onto/yso/p188880":"POINT(26.98474 63.50589)", +"http://www.yso.fi/onto/yso/p189337":"POINT(71.7787 66.3802)", +"http://www.yso.fi/onto/yso/p189359":"", +"http://www.yso.fi/onto/yso/p189884":"POINT(24.0312 62.08731)", +"http://www.yso.fi/onto/yso/p189885":"POINT(24.25037 61.53528)", +"http://www.yso.fi/onto/yso/p189886":"POINT(25.7702 64.22963)", +"http://www.yso.fi/onto/yso/p189887":"POINT(25.75372 64.2349)", +"http://www.yso.fi/onto/yso/p189889":"POINT(21.62948 61.22051)", +"http://www.yso.fi/onto/yso/p189890":"POINT(22.65188 63.23037)", +"http://www.yso.fi/onto/yso/p189891":"POINT(22.65188 63.23037)", +"http://www.yso.fi/onto/yso/p189893":"POINT(25.766666666667 54.980555555556)", +"http://www.yso.fi/onto/yso/p189894":"POINT(21.58703 60.13167)", +"http://www.yso.fi/onto/yso/p189895":"POINT(24.36057 61.6001)", +"http://www.yso.fi/onto/yso/p189896":"POINT(24.39021 61.59006)", +"http://www.yso.fi/onto/yso/p189898":"POINT(27.61799 62.79383)", +"http://www.yso.fi/onto/yso/p189904":"POINT(22.93273 61.3111)", +"http://www.yso.fi/onto/yso/p189905":"POINT(-3 53.416666666667)", +"http://www.yso.fi/onto/yso/p189935":"POINT(25.7702 64.22963)", +"http://www.yso.fi/onto/yso/p189936":"POINT(25.75372 64.2349)", +"http://www.yso.fi/onto/yso/p190456":"POINT(23.76027778 61.49777778)", +"http://www.yso.fi/onto/yso/p190458":"POINT(3.73926389 44.42623611)", +"http://www.yso.fi/onto/yso/p190511":"POINT(14.683333333333 57.65)", +"http://www.yso.fi/onto/yso/p190512":"POINT(15.75 58.416667)", +"http://www.yso.fi/onto/yso/p190513":"POINT(12.7 57.728)", +"http://www.yso.fi/onto/yso/p190706":"", +"http://www.yso.fi/onto/yso/p190711":"POINT(24.31185 61.11022)", +"http://www.yso.fi/onto/yso/p190715":"POINT(29.97677 62.05798)", +"http://www.yso.fi/onto/yso/p190716":"POINT(30.07308 61.97001)", +"http://www.yso.fi/onto/yso/p190717":"POINT(30.04741 61.98727)", +"http://www.yso.fi/onto/yso/p190718":"POINT(23.04919 61.38532)", +"http://www.yso.fi/onto/yso/p190719":"", +"http://www.yso.fi/onto/yso/p190720":"POINT(29.03266 61.22182)", +"http://www.yso.fi/onto/yso/p190721":"POINT(29.01053 61.25124)", +"http://www.yso.fi/onto/yso/p190722":"POINT(23.49982 60.65092)", +"http://www.yso.fi/onto/yso/p190726":"POINT(28.35342 68.71354)", +"http://www.yso.fi/onto/yso/p190727":"POINT(23.2492 61.71139)", +"http://www.yso.fi/onto/yso/p190728":"POINT(23.2492 61.71139)", +"http://www.yso.fi/onto/yso/p190729":"POINT(22.17754 61.44889)", +"http://www.yso.fi/onto/yso/p190730":"POINT(22.17754 61.44889)", +"http://www.yso.fi/onto/yso/p190731":"POINT(27.21014 63.38729)", +"http://www.yso.fi/onto/yso/p190732":"POINT(113 12)", +"http://www.yso.fi/onto/yso/p190733":"POINT(24.95209 60.23689)", +"http://www.yso.fi/onto/yso/p190734":"POINT(22.12954 63.03983)", +"http://www.yso.fi/onto/yso/p190736":"", +"http://www.yso.fi/onto/yso/p190813":"POINT(22.423611111111 37.081944444444)", +"http://www.yso.fi/onto/yso/p190938":"POINT(16.605555555556 47.010833333333)", +"http://www.yso.fi/onto/yso/p190992":"POINT(27.48486 69.37024)", +"http://www.yso.fi/onto/yso/p190993":"POINT(27.64383 69.43159)", +"http://www.yso.fi/onto/yso/p190994":"POINT(26.65667 69.07356)", +"http://www.yso.fi/onto/yso/p191007":"POINT(24.56728 61.50507)", +"http://www.yso.fi/onto/yso/p191008":"POINT(24.55052 61.50985)", +"http://www.yso.fi/onto/yso/p191009":"POINT(24.52231 61.52158)", +"http://www.yso.fi/onto/yso/p191269":"POINT(23.66411 61.50208)", +"http://www.yso.fi/onto/yso/p191270":"POINT(23.18871 61.46185)", +"http://www.yso.fi/onto/yso/p191271":"POINT(23.21763 61.46839)", +"http://www.yso.fi/onto/yso/p191272":"POINT(27.91562 64.4839)", +"http://www.yso.fi/onto/yso/p191273":"POINT(23.02485 60.43646)", +"http://www.yso.fi/onto/yso/p191274":"POINT(25.24506 63.81194)", +"http://www.yso.fi/onto/yso/p191275":"POINT(25.33825 65.84561)", +"http://www.yso.fi/onto/yso/p191276":"POINT(30.10226 62.80221)", +"http://www.yso.fi/onto/yso/p191277":"POINT(30.10226 62.80221)", +"http://www.yso.fi/onto/yso/p191278":"POINT(24.67114 64.62866)", +"http://www.yso.fi/onto/yso/p191279":"POINT(24.67232 64.63069)", +"http://www.yso.fi/onto/yso/p191280":"POINT(25.32664 60.2991)", +"http://www.yso.fi/onto/yso/p191281":"POINT(25.3338 60.27595)", +"http://www.yso.fi/onto/yso/p191282":"POINT(28.479166666667 60.805555555556)", +"http://www.yso.fi/onto/yso/p191285":"POINT(26.70313 60.86121)", +"http://www.yso.fi/onto/yso/p191286":"POINT(27.28812 61.58701)", +"http://www.yso.fi/onto/yso/p191287":"POINT(27.28354 61.53304)", +"http://www.yso.fi/onto/yso/p191288":"POINT(27.19478 61.55842)", +"http://www.yso.fi/onto/yso/p191289":"POINT(25.85749 62.9848)", +"http://www.yso.fi/onto/yso/p191290":"POINT(25.83209 62.93167)", +"http://www.yso.fi/onto/yso/p191531":"POINT(127.36666666667 0.78333333333333)", +"http://www.yso.fi/onto/yso/p191532":"POINT(21.99544 61.15277)", +"http://www.yso.fi/onto/yso/p191533":"POINT(8.763056 55.327222)", +"http://www.yso.fi/onto/yso/p191534":"POINT(25.29612 64.15049)", +"http://www.yso.fi/onto/yso/p191535":"POINT(24.95536 60.1857)", +"http://www.yso.fi/onto/yso/p191536":"POINT(-122.31666666667 47.6)", +"http://www.yso.fi/onto/yso/p191537":"POINT(23.72744 63.51993)", +"http://www.yso.fi/onto/yso/p192010":"POINT(21.38725 60.19155)", +"http://www.yso.fi/onto/yso/p192034":"POINT(-1.5572222222222 43.480555555556)", +"http://www.yso.fi/onto/yso/p192066":"POINT(7.0213888888889 43.5525)", +"http://www.yso.fi/onto/yso/p192067":"POINT(11.083333333333 47.5)", +"http://www.yso.fi/onto/yso/p192068":"POINT(20.483333333333 54.733333333333)", +"http://www.yso.fi/onto/yso/p192073":"POINT(28.26497 61.06358)", +"http://www.yso.fi/onto/yso/p192074":"POINT(31.343055555556 69.641388888889)", +"http://www.yso.fi/onto/yso/p192076":"POINT(25.27048 60.37745)", +"http://www.yso.fi/onto/yso/p192077":"POINT(22.31196 60.45887)", +"http://www.yso.fi/onto/yso/p192078":"POINT(8.67481 46.632513)", +"http://www.yso.fi/onto/yso/p192079":"", +"http://www.yso.fi/onto/yso/p192080":"POINT(14.555277777778 53.424722222222)", +"http://www.yso.fi/onto/yso/p192081":"POINT(14.555277777778 53.424722222222)", +"http://www.yso.fi/onto/yso/p192082":"POINT(18.283333333333 57.633333333333)", +"http://www.yso.fi/onto/yso/p192125":"", +"http://www.yso.fi/onto/yso/p192348":"", +"http://www.yso.fi/onto/yso/p192352":"POINT(24.33633 62.99403)", +"http://www.yso.fi/onto/yso/p192353":"", +"http://www.yso.fi/onto/yso/p192355":"POINT(23.82291 63.42477)", +"http://www.yso.fi/onto/yso/p192356":"POINT(56.348611 73.9525)", +"http://www.yso.fi/onto/yso/p192357":"POINT(22.06094 62.04441)", +"http://www.yso.fi/onto/yso/p192358":"POINT(22.07608 62.04884)", +"http://www.yso.fi/onto/yso/p192359":"POINT(25.19459 60.26942)", +"http://www.yso.fi/onto/yso/p192360":"POINT(24.90855 60.28309)", +"http://www.yso.fi/onto/yso/p192361":"POINT(24.56837 60.15292)", +"http://www.yso.fi/onto/yso/p192363":"POINT(22.74869 61.2963)", +"http://www.yso.fi/onto/yso/p192364":"POINT(29.41666 62.8733)", +"http://www.yso.fi/onto/yso/p192366":"", +"http://www.yso.fi/onto/yso/p192366":"POINT(22.94722222 59.77194444)", +"http://www.yso.fi/onto/yso/p192367":"", +"http://www.yso.fi/onto/yso/p192546":"POINT(-7 57.833333333333)", +"http://www.yso.fi/onto/yso/p192547":"POINT(-6 56.5)", +"http://www.yso.fi/onto/yso/p192623":"POINT(76.513333333333 35.881111111111)", +"http://www.yso.fi/onto/yso/p192726":"POINT(2.1280555555556 50.463055555556)", +"http://www.yso.fi/onto/yso/p192815":"POINT(24.99256 60.18079)", +"http://www.yso.fi/onto/yso/p193016":"POINT(28.60406 61.0061)", +"http://www.yso.fi/onto/yso/p193017":"POINT(28.53021 60.98904)", +"http://www.yso.fi/onto/yso/p193019":"POINT(28.56929 61.01534)", +"http://www.yso.fi/onto/yso/p193020":"POINT(23.8384 61.49113)", +"http://www.yso.fi/onto/yso/p193021":"POINT(16.218756 43.812761)", +"http://www.yso.fi/onto/yso/p193022":"POINT(17.40361111 42.925)", +"http://www.yso.fi/onto/yso/p193023":"POINT(26.8938 64.66075)", +"http://www.yso.fi/onto/yso/p193032":"POINT(35.383333333333 32)", +"http://www.yso.fi/onto/yso/p193033":"POINT(22.93192 62.64766)", +"http://www.yso.fi/onto/yso/p193035":"POINT(29.54041 66.36823)", +"http://www.yso.fi/onto/yso/p193036":"POINT(27.774166666667 59.393055555556)", +"http://www.yso.fi/onto/yso/p193037":"POINT(22.81469 63.27891)", +"http://www.yso.fi/onto/yso/p193038":"POINT(22.81469 63.27891)", +"http://www.yso.fi/onto/yso/p193039":"POINT(-64.75 32.333333)", +"http://www.yso.fi/onto/yso/p193040":"POINT(23.03335 61.21018)", +"http://www.yso.fi/onto/yso/p193042":"POINT(32.5887 61.8455)", +"http://www.yso.fi/onto/yso/p193124":"POINT(48.016666666667 46.333333333333)", +"http://www.yso.fi/onto/yso/p193126":"POINT(49.835183333333 40.366655555556)", +"http://www.yso.fi/onto/yso/p193140":"POINT(27.21321 63.70694)", +"http://www.yso.fi/onto/yso/p193141":"POINT(25.10357 60.17543)", +"http://www.yso.fi/onto/yso/p193232":"POINT(12.533333333333 55.677777777778)", +"http://www.yso.fi/onto/yso/p193238":"POINT(24.17621 61.40639)", +"http://www.yso.fi/onto/yso/p193372":"POINT(25.14012 61.71834)", +"http://www.yso.fi/onto/yso/p193379":"POINT(11 55.5)", +"http://www.yso.fi/onto/yso/p193380":"POINT(11 55.333)", +"http://www.yso.fi/onto/yso/p193381":"POINT(9.8333333333333 55.183333333333)", +"http://www.yso.fi/onto/yso/p193616":"POINT(21.12071 62.67999)", +"http://www.yso.fi/onto/yso/p193660":"POINT(28.18783 -25.74486)", +"http://www.yso.fi/onto/yso/p193736":"POINT(30.96289 62.74379)", +"http://www.yso.fi/onto/yso/p193769":"POINT(23.28368 60.18011)", +"http://www.yso.fi/onto/yso/p193771":"POINT(24.7316 65.93111)", +"http://www.yso.fi/onto/yso/p193772":"POINT(25.42138 64.23324)", +"http://www.yso.fi/onto/yso/p193778":"POINT(-4.2391666666667 40.24)", +"http://www.yso.fi/onto/yso/p193779":"POINT(25.5566 64.71496)", +"http://www.yso.fi/onto/yso/p193780":"POINT(25.5566 64.71496)", +"http://www.yso.fi/onto/yso/p193781":"POINT(29.80237 63.18917)", +"http://www.yso.fi/onto/yso/p193782":"POINT(26.79729 60.77839)", +"http://www.yso.fi/onto/yso/p193788":"POINT(22.53589 60.0043)", +"http://www.yso.fi/onto/yso/p194164":"POINT(29.41666667 70.41666667)", +"http://www.yso.fi/onto/yso/p194270":"POINT(13.35 57.483333333333)", +"http://www.yso.fi/onto/yso/p194358":"POINT(17.2 59.5)", +"http://www.yso.fi/onto/yso/p194407":"POINT(28.35365 61.07567)", +"http://www.yso.fi/onto/yso/p194408":"POINT(28.38505 61.0739)", +"http://www.yso.fi/onto/yso/p194410":"POINT(28.41542 61.08677)", +"http://www.yso.fi/onto/yso/p194412":"POINT(24.59147 65.70939)", +"http://www.yso.fi/onto/yso/p194413":"POINT(25.31192 61.8112)", +"http://www.yso.fi/onto/yso/p194414":"POINT(25.37254 61.78256)", +"http://www.yso.fi/onto/yso/p194417":"POINT(27.69834 62.98243)", +"http://www.yso.fi/onto/yso/p194418":"POINT(27.76198 63.1267)", +"http://www.yso.fi/onto/yso/p194419":"POINT(27.77726 63.17049)", +"http://www.yso.fi/onto/yso/p194420":"POINT(27.8248 63.08081)", +"http://www.yso.fi/onto/yso/p194422":"POINT(24.03005 61.12026)", +"http://www.yso.fi/onto/yso/p194423":"POINT(24.86472 60.22736)", +"http://www.yso.fi/onto/yso/p194425":"POINT(22.51074 62.24724)", +"http://www.yso.fi/onto/yso/p194427":"POINT(24.12718 63.24781)", +"http://www.yso.fi/onto/yso/p194428":"POINT(22.55537 63.57033)", +"http://www.yso.fi/onto/yso/p194429":"POINT(25.15898 61.52214)", +"http://www.yso.fi/onto/yso/p194430":"POINT(29.806944444444 60.404166666667)", +"http://www.yso.fi/onto/yso/p194432":"POINT(22.55335 63.34947)", +"http://www.yso.fi/onto/yso/p194436":"POINT(19.58109 60.18741)", +"http://www.yso.fi/onto/yso/p194437":"POINT(19.52352 60.17376)", +"http://www.yso.fi/onto/yso/p194439":"POINT(39.826111111111 21.4225)", +"http://www.yso.fi/onto/yso/p194689":"POINT(24.34123 60.19903)", +"http://www.yso.fi/onto/yso/p194770":"POINT(22.348611111111 70.239444444444)", +"http://www.yso.fi/onto/yso/p194772":"POINT(22.3506 70.2408)", +"http://www.yso.fi/onto/yso/p194783":"POINT(-5.9833333333333 37.383333333333)", +"http://www.yso.fi/onto/yso/p194809":"POINT(24.54859 61.014)", +"http://www.yso.fi/onto/yso/p194810":"POINT(24.57132 61.01325)", +"http://www.yso.fi/onto/yso/p194885":"POINT(25.73492 60.9973)", +"http://www.yso.fi/onto/yso/p194886":"", +"http://www.yso.fi/onto/yso/p194887":"POINT(29.333333333333 60.597222222222)", +"http://www.yso.fi/onto/yso/p194888":"POINT(4.5972 52.3644)", +"http://www.yso.fi/onto/yso/p194891":"POINT(24.96179 61.09908)", +"http://www.yso.fi/onto/yso/p194893":"POINT(22.5 38.483333333333)", +"http://www.yso.fi/onto/yso/p194894":"POINT(42.433333 11.8)", +"http://www.yso.fi/onto/yso/p194895":"POINT(30.966666666667 66.083333333333)", +"http://www.yso.fi/onto/yso/p194898":"POINT(110.58888888889 0.11944444444444)", +"http://www.yso.fi/onto/yso/p194899":"POINT(21.78968 61.49373)", +"http://www.yso.fi/onto/yso/p194900":"POINT(27.39166 60.55494)", +"http://www.yso.fi/onto/yso/p195061":"POINT(4 49)", +"http://www.yso.fi/onto/yso/p195062":"POINT(4.5 47)", +"http://www.yso.fi/onto/yso/p195206":"POINT(25.43175 65.11793)", +"http://www.yso.fi/onto/yso/p195327":"POINT(23.0742 62.25751)", +"http://www.yso.fi/onto/yso/p195329":"POINT(24.88948 60.77095)", +"http://www.yso.fi/onto/yso/p195331":"POINT(25.12787 60.2299)", +"http://www.yso.fi/onto/yso/p195333":"POINT(24.57526 64.07481)", +"http://www.yso.fi/onto/yso/p195334":"POINT(24.54044 64.06165)", +"http://www.yso.fi/onto/yso/p195335":"POINT(24.56536 64.04898)", +"http://www.yso.fi/onto/yso/p195337":"POINT(22.56727 61.4402)", +"http://www.yso.fi/onto/yso/p195338":"POINT(23.62224 61.64836)", +"http://www.yso.fi/onto/yso/p195342":"", +"http://www.yso.fi/onto/yso/p195343":"POINT(24.81377 63.66764)", +"http://www.yso.fi/onto/yso/p195344":"POINT(23.76511 59.93309)", +"http://www.yso.fi/onto/yso/p195416":"POINT(98.398333333333 7.89)", +"http://www.yso.fi/onto/yso/p195421":"POINT(22.95452 61.33048)", +"http://www.yso.fi/onto/yso/p195422":"POINT(22.94423 61.33745)", +"http://www.yso.fi/onto/yso/p195423":"POINT(29.10007 61.80402)", +"http://www.yso.fi/onto/yso/p195603":"POINT(18.15312 54.29443)", +"http://www.yso.fi/onto/yso/p195651":"POINT(35.323197222222 63.313861111111)", +"http://www.yso.fi/onto/yso/p195788":"POINT(21.14317 60.79454)", +"http://www.yso.fi/onto/yso/p195808":"POINT(-97 55)", +"http://www.yso.fi/onto/yso/p195809":"POINT(24.78504 61.17292)", +"http://www.yso.fi/onto/yso/p195810":"POINT(24.78186 61.18592)", +"http://www.yso.fi/onto/yso/p195811":"POINT(24.81823 61.20336)", +"http://www.yso.fi/onto/yso/p195812":"POINT(24.80939 61.13631)", +"http://www.yso.fi/onto/yso/p195813":"POINT(24.85224 61.06481)", +"http://www.yso.fi/onto/yso/p195814":"POINT(24.93746 60.44946)", +"http://www.yso.fi/onto/yso/p195815":"POINT(22.72556 63.75393)", +"http://www.yso.fi/onto/yso/p195816":"POINT(22.8215 59.98229)", +"http://www.yso.fi/onto/yso/p195817":"POINT(28.42071 66.37417)", +"http://www.yso.fi/onto/yso/p195818":"POINT(17.066666666667 61.983333333333)", +"http://www.yso.fi/onto/yso/p195819":"POINT(23.48728 62.84392)", +"http://www.yso.fi/onto/yso/p196053":"POINT(-14.003056 28.4325)", +"http://www.yso.fi/onto/yso/p196054":"POINT(45 53.2)", +"http://www.yso.fi/onto/yso/p196118":"POINT(33.75 35.25)", +"http://www.yso.fi/onto/yso/p196125":"POINT(27.6 57.816666666667)", +"http://www.yso.fi/onto/yso/p196126":"POINT(27.19965 62.35235)", +"http://www.yso.fi/onto/yso/p196146":"POINT(25.23571 60.61326)", +"http://www.yso.fi/onto/yso/p196355":"POINT(23.78182 61.31393)", +"http://www.yso.fi/onto/yso/p196364":"POINT(88.366666666667 22.566666666667)", +"http://www.yso.fi/onto/yso/p196387":"POINT(22.8574 63.63549)", +"http://www.yso.fi/onto/yso/p196389":"POINT(22.0189 60.55903)", +"http://www.yso.fi/onto/yso/p196390":"POINT(27.933333333333 60.591666666667)", +"http://www.yso.fi/onto/yso/p196391":"POINT(27.76364 60.62416)", +"http://www.yso.fi/onto/yso/p196392":"POINT(27.69195 60.52194)", +"http://www.yso.fi/onto/yso/p196393":"POINT(27.76882 60.61401)", +"http://www.yso.fi/onto/yso/p196395":"POINT(107.04819 20.72779)", +"http://www.yso.fi/onto/yso/p196396":"POINT(26.4995 60.32898)", +"http://www.yso.fi/onto/yso/p196397":"POINT(24.83212 60.10169)", +"http://www.yso.fi/onto/yso/p196470":"POINT(24.47318 60.96604)", +"http://www.yso.fi/onto/yso/p196506":"POINT(28.977777777778 60.918055555556)", +"http://www.yso.fi/onto/yso/p196611":"POINT(24.9907 60.23936)", +"http://www.yso.fi/onto/yso/p196629":"POINT(23.84042 61.63065)", +"http://www.yso.fi/onto/yso/p196635":"POINT(25.39484 60.38075)", +"http://www.yso.fi/onto/yso/p196636":"POINT(25.43396 60.39513)", +"http://www.yso.fi/onto/yso/p196638":"POINT(25.84814 60.33696)", +"http://www.yso.fi/onto/yso/p196762":"POINT(24.79171 61.09734)", +"http://www.yso.fi/onto/yso/p196763":"POINT(-69.416666666667 -15.75)", +"http://www.yso.fi/onto/yso/p196764":"POINT(-71.978056 -13.518333)", +"http://www.yso.fi/onto/yso/p196765":"POINT(-72.545555555556 -13.163055555556)", +"http://www.yso.fi/onto/yso/p196766":"POINT(25 39)", +"http://www.yso.fi/onto/yso/p196767":"", +"http://www.yso.fi/onto/yso/p196767":"POINT(25.3672 64.138246)", +"http://www.yso.fi/onto/yso/p196768":"", +"http://www.yso.fi/onto/yso/p196768":"POINT(24.9642 63.9293)", +"http://www.yso.fi/onto/yso/p196769":"POINT(21.95993 61.04695)", +"http://www.yso.fi/onto/yso/p196770":"POINT(21.95993 61.04695)", +"http://www.yso.fi/onto/yso/p196771":"POINT(27.1099 60.56587)", +"http://www.yso.fi/onto/yso/p196773":"POINT(24.3979 61.47186)", +"http://www.yso.fi/onto/yso/p196777":"", +"http://www.yso.fi/onto/yso/p196779":"POINT(29.997222222222 60.677777777778)", +"http://www.yso.fi/onto/yso/p196781":"POINT(25.79619 63.59422)", +"http://www.yso.fi/onto/yso/p196782":"POINT(25.72765 63.57209)", +"http://www.yso.fi/onto/yso/p196783":"POINT(25.5431 63.55413)", +"http://www.yso.fi/onto/yso/p196784":"POINT(25.46492 63.61282)", +"http://www.yso.fi/onto/yso/p196787":"POINT(28.552777777778 60.434722222222)", +"http://www.yso.fi/onto/yso/p196800":"POINT(29.58288 66.14673)", +"http://www.yso.fi/onto/yso/p196802":"POINT(22.08044 60.71345)", +"http://www.yso.fi/onto/yso/p196804":"POINT(25.69646 60.2931)", +"http://www.yso.fi/onto/yso/p196807":"POINT(27.69439 64.25576)", +"http://www.yso.fi/onto/yso/p196809":"POINT(27.71174 64.24519)", +"http://www.yso.fi/onto/yso/p196812":"POINT(24.57617 60.94313)", +"http://www.yso.fi/onto/yso/p196970":"POINT(26.361111111111 59.350555555556)", +"http://www.yso.fi/onto/yso/p197130":"POINT(24.78737 61.1466)", +"http://www.yso.fi/onto/yso/p197131":"POINT(24.80261 61.14647)", +"http://www.yso.fi/onto/yso/p197132":"POINT(24.76911 61.16856)", +"http://www.yso.fi/onto/yso/p197133":"POINT(25.59366 61.6072)", +"http://www.yso.fi/onto/yso/p197134":"POINT(25.72427 61.44955)", +"http://www.yso.fi/onto/yso/p197135":"POINT(21.96639 60.13139)", +"http://www.yso.fi/onto/yso/p197138":"", +"http://www.yso.fi/onto/yso/p197141":"POINT(26.92885 60.49861)", +"http://www.yso.fi/onto/yso/p197142":"POINT(25.3007 62.48545)", +"http://www.yso.fi/onto/yso/p197143":"POINT(25.31259 62.51181)", +"http://www.yso.fi/onto/yso/p197144":"POINT(25.32533 62.53208)", +"http://www.yso.fi/onto/yso/p197145":"POINT(25.25174 62.18139)", +"http://www.yso.fi/onto/yso/p197146":"POINT(25.18126 62.2078)", +"http://www.yso.fi/onto/yso/p197147":"POINT(25.1506 62.18798)", +"http://www.yso.fi/onto/yso/p197148":"POINT(25.23984 62.22759)", +"http://www.yso.fi/onto/yso/p197149":"POINT(25.01829 62.20373)", +"http://www.yso.fi/onto/yso/p197151":"POINT(25.61405 61.1914)", +"http://www.yso.fi/onto/yso/p197152":"POINT(24.45562 60.11753)", +"http://www.yso.fi/onto/yso/p197153":"POINT(25.12852 61.48848)", +"http://www.yso.fi/onto/yso/p197156":"POINT(25.05979 65.61858)", +"http://www.yso.fi/onto/yso/p197158":"POINT(21.92526 62.48058)", +"http://www.yso.fi/onto/yso/p197256":"POINT(-7.98108 31.62947)", +"http://www.yso.fi/onto/yso/p197727":"POINT(21.34173 60.9294)", +"http://www.yso.fi/onto/yso/p197728":"POINT(21.34173 60.9294)", +"http://www.yso.fi/onto/yso/p197825":"POINT(22.47079 60.06525)", +"http://www.yso.fi/onto/yso/p197826":"POINT(30.04949 62.581)", +"http://www.yso.fi/onto/yso/p197828":"POINT(28.445833333333 60.599166666667)", +"http://www.yso.fi/onto/yso/p197831":"POINT(21.45986 61.06015)", +"http://www.yso.fi/onto/yso/p197832":"POINT(21.45986 61.06015)", +"http://www.yso.fi/onto/yso/p197834":"POINT(19.96298 60.3406)", +"http://www.yso.fi/onto/yso/p197835":"POINT(19.98476 60.35852)", +"http://www.yso.fi/onto/yso/p197836":"POINT(19.96184 60.34647)", +"http://www.yso.fi/onto/yso/p197837":"POINT(19.94497 60.33103)", +"http://www.yso.fi/onto/yso/p197838":"POINT(19.91808 60.34103)", +"http://www.yso.fi/onto/yso/p197839":"", +"http://www.yso.fi/onto/yso/p197840":"POINT(24.77027 63.97599)", +"http://www.yso.fi/onto/yso/p197843":"POINT(21.29658 62.78998)", +"http://www.yso.fi/onto/yso/p197845":"POINT(23.14082 62.9492)", +"http://www.yso.fi/onto/yso/p197861":"POINT(23.82058 61.48491)", +"http://www.yso.fi/onto/yso/p197901":"POINT(80.27 13.09)", +"http://www.yso.fi/onto/yso/p198197":"POINT(39.720277777778 43.585277777778)", +"http://www.yso.fi/onto/yso/p198297":"POINT(12.566666666667 42.983333333333)", +"http://www.yso.fi/onto/yso/p198422":"POINT(22.86447 63.78072)", +"http://www.yso.fi/onto/yso/p198828":"POINT(23.80889 66.18671)", +"http://www.yso.fi/onto/yso/p198829":"POINT(23.92517 62.12225)", +"http://www.yso.fi/onto/yso/p198830":"POINT(26.41944 64.63097)", +"http://www.yso.fi/onto/yso/p198831":"POINT(15.433333333333 47.066666666667)", +"http://www.yso.fi/onto/yso/p198832":"POINT(25.11168 62.84565)", +"http://www.yso.fi/onto/yso/p198834":"POINT(25.61749 61.34806)", +"http://www.yso.fi/onto/yso/p198835":"POINT(25.6578 61.40441)", +"http://www.yso.fi/onto/yso/p198836":"POINT(25.6554 62.39585)", +"http://www.yso.fi/onto/yso/p198837":"POINT(25.60638889 62.40888889)", +"http://www.yso.fi/onto/yso/p198838":"POINT(27.23453 63.57649)", +"http://www.yso.fi/onto/yso/p198895":"POINT(24.955 60.167)", +"http://www.yso.fi/onto/yso/p199000":"POINT(22.53706 61.55521)", +"http://www.yso.fi/onto/yso/p199001":"POINT(22.46069 61.55302)", +"http://www.yso.fi/onto/yso/p199347":"POINT(25.07524 60.1856)", +"http://www.yso.fi/onto/yso/p199373":"POINT(-0.37055555555556 49.182222222222)", +"http://www.yso.fi/onto/yso/p199390":"POINT(22.36388 61.90569)", +"http://www.yso.fi/onto/yso/p199395":"POINT(28.30832 61.09865)", +"http://www.yso.fi/onto/yso/p199396":"POINT(28.30832 61.09865)", +"http://www.yso.fi/onto/yso/p199397":"POINT(67.01 30.12)", +"http://www.yso.fi/onto/yso/p199412":"POINT(24.9749 60.18002)", +"http://www.yso.fi/onto/yso/p199422":"POINT(-123.1 49.25)", +"http://www.yso.fi/onto/yso/p199430":"POINT(-69 45.5)", +"http://www.yso.fi/onto/yso/p199485":"POINT(25.65862 60.40092)", +"http://www.yso.fi/onto/yso/p199503":"POINT(9.155 45.185280555556)", +"http://www.yso.fi/onto/yso/p199509":"", +"http://www.yso.fi/onto/yso/p199541":"POINT(28.10944 61.93088)", +"http://www.yso.fi/onto/yso/p199595":"POINT(28.57255 64.93138)", +"http://www.yso.fi/onto/yso/p199597":"POINT(22.21255 61.4317)", +"http://www.yso.fi/onto/yso/p199599":"POINT(22.08783 61.48061)", +"http://www.yso.fi/onto/yso/p199600":"POINT(22.04428 63.1254)", +"http://www.yso.fi/onto/yso/p199608":"POINT(-70.306425 44.205555555556)", +"http://www.yso.fi/onto/yso/p199609":"POINT(-71.8 42.3)", +"http://www.yso.fi/onto/yso/p199610":"POINT(-71.5 44)", +"http://www.yso.fi/onto/yso/p199611":"POINT(-72.7 44)", +"http://www.yso.fi/onto/yso/p199612":"POINT(-71.5 41.7)", +"http://www.yso.fi/onto/yso/p199613":"POINT(-72.7 41.6)", +"http://www.yso.fi/onto/yso/p199630":"POINT(26.37352 60.5095)", +"http://www.yso.fi/onto/yso/p199654":"POINT(24.23909 60.23602)", +"http://www.yso.fi/onto/yso/p199655":"POINT(23.72781 60.32405)", +"http://www.yso.fi/onto/yso/p199656":"POINT(25.41367 60.26591)", +"http://www.yso.fi/onto/yso/p199699":"POINT(22.09514 60.46126)", +"http://www.yso.fi/onto/yso/p199724":"POINT(78.461666666667 31.004722222222)", +"http://www.yso.fi/onto/yso/p199812":"POINT(13 56.75)", +"http://www.yso.fi/onto/yso/p199813":"POINT(24.41766 60.26592)", +"http://www.yso.fi/onto/yso/p199814":"POINT(21.97683 60.48019)", +"http://www.yso.fi/onto/yso/p199831":"POINT(23.06815 60.37131)", +"http://www.yso.fi/onto/yso/p199839":"POINT(27.91435 62.15334)", +"http://www.yso.fi/onto/yso/p199889":"POINT(23.07365 60.2938)", +"http://www.yso.fi/onto/yso/p199891":"POINT(21.68872 60.77639)", +"http://www.yso.fi/onto/yso/p199892":"POINT(21.68872 60.77639)", +"http://www.yso.fi/onto/yso/p199939":"POINT(127.749023 26.332807)", +"http://www.yso.fi/onto/yso/p199940":"POINT(127.95 26.483333333333)", +"http://www.yso.fi/onto/yso/p199942":"POINT(20.5 38.5)", +"http://www.yso.fi/onto/yso/p200016":"POINT(20.4 39.283333333333)", +"http://www.yso.fi/onto/yso/p200137":"POINT(23.01273 61.26127)", +"http://www.yso.fi/onto/yso/p200170":"POINT(21.37474 59.77895)", +"http://www.yso.fi/onto/yso/p200171":"POINT(24.2048 62.13147)", +"http://www.yso.fi/onto/yso/p200172":"POINT(24.2048 62.13147)", +"http://www.yso.fi/onto/yso/p200173":"POINT(22.23672 61.17517)", +"http://www.yso.fi/onto/yso/p200570":"POINT(24.15192 67.60946)", +"http://www.yso.fi/onto/yso/p200621":"POINT(25.36742 66.62587)", +"http://www.yso.fi/onto/yso/p200623":"POINT(22.99703 62.95248)", +"http://www.yso.fi/onto/yso/p200629":"POINT(21.76041 60.48682)", +"http://www.yso.fi/onto/yso/p200630":"POINT(23.16537 63.83469)", +"http://www.yso.fi/onto/yso/p200632":"POINT(22.61476 60.38552)", +"http://www.yso.fi/onto/yso/p200633":"POINT(12.133333333333 54.083333333333)", +"http://www.yso.fi/onto/yso/p200634":"POINT(11.465 53.8925)", +"http://www.yso.fi/onto/yso/p200635":"POINT(22.3054 60.51449)", +"http://www.yso.fi/onto/yso/p200646":"POINT(14.733333333333 60.866666666667)", +"http://www.yso.fi/onto/yso/p200680":"POINT(15.616666666667 62.516666666667)", +"http://www.yso.fi/onto/yso/p200681":"POINT(28.121 66.36048)", +"http://www.yso.fi/onto/yso/p200684":"POINT(27.60298 62.27747)", +"http://www.yso.fi/onto/yso/p200685":"POINT(28.28847 62.06756)", +"http://www.yso.fi/onto/yso/p200725":"POINT(21.48402 61.14349)", +"http://www.yso.fi/onto/yso/p200777":"POINT(25.98578 63.68184)", +"http://www.yso.fi/onto/yso/p200834":"POINT(31.689811111111 54.770088888889)", +"http://www.yso.fi/onto/yso/p200842":"POINT(26.730888888889 61.055527777778)", +"http://www.yso.fi/onto/yso/p200843":"POINT(26.75278 61.11048)", +"http://www.yso.fi/onto/yso/p200876":"POINT(27.65959 60.68482)", +"http://www.yso.fi/onto/yso/p200879":"POINT(27.70258 60.65545)", +"http://www.yso.fi/onto/yso/p200880":"POINT(26.40286 60.68643)", +"http://www.yso.fi/onto/yso/p200881":"POINT(26.57472 60.8549)", +"http://www.yso.fi/onto/yso/p200883":"POINT(26.4118 60.89926)", +"http://www.yso.fi/onto/yso/p200886":"POINT(26.41227 60.88263)", +"http://www.yso.fi/onto/yso/p200887":"POINT(26.50302 60.89851)", +"http://www.yso.fi/onto/yso/p201029":"POINT(29.05455 61.92991)", +"http://www.yso.fi/onto/yso/p201030":"POINT(29.03195 61.92867)", +"http://www.yso.fi/onto/yso/p201031":"POINT(29.05107 61.91571)", +"http://www.yso.fi/onto/yso/p201032":"POINT(27.31528 61.54723)", +"http://www.yso.fi/onto/yso/p201122":"POINT(27.84423 62.16137)", +"http://www.yso.fi/onto/yso/p201170":"POINT(104.16667 0.45)", +"http://www.yso.fi/onto/yso/p201210":"POINT(28.3599 61.79227)", +"http://www.yso.fi/onto/yso/p201211":"POINT(19.5 63.566666666667)", +"http://www.yso.fi/onto/yso/p201213":"POINT(25.47083333 60.81805556)", +"http://www.yso.fi/onto/yso/p201216":"POINT(26.59012 61.25265)", +"http://www.yso.fi/onto/yso/p201217":"POINT(26.54044 61.26415)", +"http://www.yso.fi/onto/yso/p201219":"POINT(15.133333333333 60.883333333333)", +"http://www.yso.fi/onto/yso/p201220":"POINT(22.65012 63.67293)", +"http://www.yso.fi/onto/yso/p201222":"POINT(26.6434 62.42288)", +"http://www.yso.fi/onto/yso/p201223":"POINT(26.58603 62.45137)", +"http://www.yso.fi/onto/yso/p201225":"POINT(35.915683 67.792885)", +"http://www.yso.fi/onto/yso/p201226":"POINT(28.03933 63.31975)", +"http://www.yso.fi/onto/yso/p201227":"POINT(22.89325 63.67029)", +"http://www.yso.fi/onto/yso/p201230":"POINT(27.68843 62.92614)", +"http://www.yso.fi/onto/yso/p201233":"POINT(26.67848 61.70998)", +"http://www.yso.fi/onto/yso/p201406":"POINT(21.9873 62.98647)", +"http://www.yso.fi/onto/yso/p201407":"POINT(21.99297 62.98047)", +"http://www.yso.fi/onto/yso/p201409":"POINT(24.4356 61.00752)", +"http://www.yso.fi/onto/yso/p201416":"POINT(25.583333333333 58.333333333333)", +"http://www.yso.fi/onto/yso/p201417":"", +"http://www.yso.fi/onto/yso/p201435":"POINT(17.08361111 -22.57)", +"http://www.yso.fi/onto/yso/p201488":"", +"http://www.yso.fi/onto/yso/p201597":"POINT(23.933333333333 54.9)", +"http://www.yso.fi/onto/yso/p201621":"POINT(24.64087 61.02282)", +"http://www.yso.fi/onto/yso/p201630":"POINT(26.03156 67.50993)", +"http://www.yso.fi/onto/yso/p201658":"POINT(23.68981 61.46764)", +"http://www.yso.fi/onto/yso/p201673":"", +"http://www.yso.fi/onto/yso/p201683":"POINT(23.77574 61.75286)", +"http://www.yso.fi/onto/yso/p201684":"POINT(23.77574 61.75286)", +"http://www.yso.fi/onto/yso/p201694":"POINT(25.67954 62.34036)", +"http://www.yso.fi/onto/yso/p201695":"POINT(25.69859 62.3817)", +"http://www.yso.fi/onto/yso/p201696":"POINT(25.46854 62.35458)", +"http://www.yso.fi/onto/yso/p201697":"POINT(25.59478 62.38232)", +"http://www.yso.fi/onto/yso/p201699":"POINT(25.76149 62.37478)", +"http://www.yso.fi/onto/yso/p201919":"POINT(-3.1666666666667 54.5)", +"http://www.yso.fi/onto/yso/p201922":"", +"http://www.yso.fi/onto/yso/p201928":"POINT(23.30781 60.66699)", +"http://www.yso.fi/onto/yso/p201969":"POINT(24.63284 60.83192)", +"http://www.yso.fi/onto/yso/p201970":"POINT(24.59769 60.88412)", +"http://www.yso.fi/onto/yso/p201971":"POINT(24.5134 60.87574)", +"http://www.yso.fi/onto/yso/p201975":"POINT(26.57257 60.79699)", +"http://www.yso.fi/onto/yso/p201978":"POINT(19.833333333333 66.616666666667)", +"http://www.yso.fi/onto/yso/p201980":"POINT(26.04074 61.19788)", +"http://www.yso.fi/onto/yso/p201984":"POINT(22.31408 60.4303)", +"http://www.yso.fi/onto/yso/p202085":"POINT(22.00542 63.18204)", +"http://www.yso.fi/onto/yso/p202097":"POINT(26.09676 62.24134)", +"http://www.yso.fi/onto/yso/p202111":"POINT(24.45485 60.22192)", +"http://www.yso.fi/onto/yso/p202114":"POINT(24.48828 64.11034)", +"http://www.yso.fi/onto/yso/p202117":"", +"http://www.yso.fi/onto/yso/p202118":"POINT(23.18719 60.42194)", +"http://www.yso.fi/onto/yso/p202133":"POINT(24.75357 60.21607)", +"http://www.yso.fi/onto/yso/p202225":"POINT(22.26286 63.2)", +"http://www.yso.fi/onto/yso/p202227":"POINT(25.52192 65.00652)", +"http://www.yso.fi/onto/yso/p202237":"POINT(26.16072 60.54804)", +"http://www.yso.fi/onto/yso/p202241":"POINT(26.16521 60.52999)", +"http://www.yso.fi/onto/yso/p202242":"POINT(26.19207 60.53244)", +"http://www.yso.fi/onto/yso/p202292":"", +"http://www.yso.fi/onto/yso/p202432":"POINT(21.57621 60.97633)", +"http://www.yso.fi/onto/yso/p202434":"POINT(21.96397 61.12264)", +"http://www.yso.fi/onto/yso/p202435":"POINT(21.96056 61.11886)", +"http://www.yso.fi/onto/yso/p202436":"POINT(25.63732 65.14791)", +"http://www.yso.fi/onto/yso/p202483":"POINT(44.116666666667 49.733333333333)", +"http://www.yso.fi/onto/yso/p202484":"POINT(24.93816667 60.16466667)", +"http://www.yso.fi/onto/yso/p202486":"POINT(29.89339 61.99624)", +"http://www.yso.fi/onto/yso/p202487":"POINT(23.27177 61.77586)", +"http://www.yso.fi/onto/yso/p202489":"POINT(27.51825 64.40576)", +"http://www.yso.fi/onto/yso/p202490":"POINT(27.41635 64.41126)", +"http://www.yso.fi/onto/yso/p202491":"POINT(27.56644 64.36199)", +"http://www.yso.fi/onto/yso/p202495":"POINT(26.75351 62.10187)", +"http://www.yso.fi/onto/yso/p202496":"POINT(22.30362 60.31046)", +"http://www.yso.fi/onto/yso/p202525":"POINT(26.66113 64.86764)", +"http://www.yso.fi/onto/yso/p202536":"", +"http://www.yso.fi/onto/yso/p202914":"POINT(23.59202 61.72245)", +"http://www.yso.fi/onto/yso/p202935":"POINT(23.33171 60.80312)", +"http://www.yso.fi/onto/yso/p202945":"POINT(24.66278 61.27689)", +"http://www.yso.fi/onto/yso/p202972":"POINT(27.45833 66.79153)", +"http://www.yso.fi/onto/yso/p202974":"POINT(24.84377 60.60803)", +"http://www.yso.fi/onto/yso/p202975":"POINT(28.59532 64.11724)", +"http://www.yso.fi/onto/yso/p203002":"POINT(28.79534 61.1366)", +"http://www.yso.fi/onto/yso/p203003":"POINT(28.78181 61.13688)", +"http://www.yso.fi/onto/yso/p203076":"POINT(28.566666666667 60.616666666667)", +"http://www.yso.fi/onto/yso/p203092":"POINT(24.0259 61.91194)", +"http://www.yso.fi/onto/yso/p203093":"POINT(23.56222 62.01473)", +"http://www.yso.fi/onto/yso/p203094":"POINT(23.56222 62.01473)", +"http://www.yso.fi/onto/yso/p203150":"POINT(28.63813 61.10275)", +"http://www.yso.fi/onto/yso/p203151":"POINT(28.63813 61.10275)", +"http://www.yso.fi/onto/yso/p203152":"POINT(27.70217 63.21743)", +"http://www.yso.fi/onto/yso/p203172":"POINT(-76.615 39.286388888889)", +"http://www.yso.fi/onto/yso/p203272":"POINT(-81.8 24.55)", +"http://www.yso.fi/onto/yso/p203298":"POINT(24.47387 60.01195)", +"http://www.yso.fi/onto/yso/p203299":"POINT(22.88429 61.23277)", +"http://www.yso.fi/onto/yso/p203300":"POINT(15.416666666667 62.75)", +"http://www.yso.fi/onto/yso/p203497":"POINT(26.85123 60.69903)", +"http://www.yso.fi/onto/yso/p203499":"POINT(25 70)", +"http://www.yso.fi/onto/yso/p203555":"POINT(33.75 63.3)", +"http://www.yso.fi/onto/yso/p203572":"POINT(32.83611111 61.57222222)", +"http://www.yso.fi/onto/yso/p203585":"POINT(52 26)", +"http://www.yso.fi/onto/yso/p203700":"POINT(22.1965 60.58712)", +"http://www.yso.fi/onto/yso/p204131":"POINT(23.333333333333 42.7)", +"http://www.yso.fi/onto/yso/p204132":"POINT(15.95 45.8)", +"http://www.yso.fi/onto/yso/p204133":"POINT(16.2 59.76666667)", +"http://www.yso.fi/onto/yso/p204134":"POINT(24.10349 63.18498)", +"http://www.yso.fi/onto/yso/p204136":"POINT(33 -1)", +"http://www.yso.fi/onto/yso/p204198":"POINT(24.99986 60.19676)", +"http://www.yso.fi/onto/yso/p204312":"POINT(21.7689 61.4855)", +"http://www.yso.fi/onto/yso/p204686":"POINT(25.86967 62.24786)", +"http://www.yso.fi/onto/yso/p204727":"POINT(25.56374 64.99977)", +"http://www.yso.fi/onto/yso/p204728":"POINT(27.56539 66.63293)", +"http://www.yso.fi/onto/yso/p204735":"POINT(29.27229 62.10908)", +"http://www.yso.fi/onto/yso/p204736":"POINT(29.27229 62.10908)", +"http://www.yso.fi/onto/yso/p204744":"POINT(24.3264 64.39263)", +"http://www.yso.fi/onto/yso/p204745":"POINT(22.41494 61.01178)", +"http://www.yso.fi/onto/yso/p204753":"POINT(24.55465 65.94138)", +"http://www.yso.fi/onto/yso/p204777":"POINT(22.18833 60.45176)", +"http://www.yso.fi/onto/yso/p204778":"POINT(22.20519 60.45578)", +"http://www.yso.fi/onto/yso/p204779":"POINT(22.16223 60.45571)", +"http://www.yso.fi/onto/yso/p204781":"POINT(29.84351 63.79354)", +"http://www.yso.fi/onto/yso/p204783":"POINT(26.68723 66.37065)", +"http://www.yso.fi/onto/yso/p204784":"POINT(26.56865 66.46306)", +"http://www.yso.fi/onto/yso/p204785":"POINT(26.44693 66.40122)", +"http://www.yso.fi/onto/yso/p204786":"POINT(27.1494 66.3433)", +"http://www.yso.fi/onto/yso/p204787":"POINT(26.87957 66.34701)", +"http://www.yso.fi/onto/yso/p204788":"POINT(27.10814 66.321)", +"http://www.yso.fi/onto/yso/p204789":"POINT(25.42593 66.62499)", +"http://www.yso.fi/onto/yso/p204790":"POINT(26.94917 66.32408)", +"http://www.yso.fi/onto/yso/p204791":"POINT(27.1736 66.26163)", +"http://www.yso.fi/onto/yso/p204989":"POINT(25.02691 60.24838)", +"http://www.yso.fi/onto/yso/p204990":"POINT(21.82736 61.46041)", +"http://www.yso.fi/onto/yso/p204991":"POINT(21.85476 61.47532)", +"http://www.yso.fi/onto/yso/p205201":"POINT(12.716666666667 41.9)", +"http://www.yso.fi/onto/yso/p205340":"POINT(29.3321 62.81671)", +"http://www.yso.fi/onto/yso/p205341":"POINT(30.05269 62.6162)", +"http://www.yso.fi/onto/yso/p205342":"POINT(30.09987 63.13876)", +"http://www.yso.fi/onto/yso/p205463":"POINT(-90.55 -0.66666666666667)", +"http://www.yso.fi/onto/yso/p205505":"POINT(24.8791 60.22592)", +"http://www.yso.fi/onto/yso/p205506":"POINT(24.85871 60.21779)", +"http://www.yso.fi/onto/yso/p205510":"POINT(27.43411 67.49041)", +"http://www.yso.fi/onto/yso/p205511":"POINT(7.5 51.5)", +"http://www.yso.fi/onto/yso/p205512":"POINT(29.40455 65.90166)", +"http://www.yso.fi/onto/yso/p205515":"POINT(9.95 45.65)", +"http://www.yso.fi/onto/yso/p205516":"POINT(11 60)", +"http://www.yso.fi/onto/yso/p205517":"POINT(17.8125 43.349444444444)", +"http://www.yso.fi/onto/yso/p205518":"POINT(20.466666666667 44.816666666667)", +"http://www.yso.fi/onto/yso/p205519":"POINT(21.166666666667 42.666666666667)", +"http://www.yso.fi/onto/yso/p205520":"POINT(21.433333333333 41.983333333333)", +"http://www.yso.fi/onto/yso/p205595":"POINT(-7.62 33.599166666667)", +"http://www.yso.fi/onto/yso/p205596":"POINT(-112.095278 36.0975)", +"http://www.yso.fi/onto/yso/p205599":"POINT(27.83159 63.81031)", +"http://www.yso.fi/onto/yso/p205601":"POINT(25.68289 62.49091)", +"http://www.yso.fi/onto/yso/p205602":"POINT(82.7161 71.8297)", +"http://www.yso.fi/onto/yso/p205603":"POINT(32.467965694444 57.251331194444)", +"http://www.yso.fi/onto/yso/p205604":"POINT(27.92125 63.84386)", +"http://www.yso.fi/onto/yso/p205605":"POINT(-89.971111111111 35.1175)", +"http://www.yso.fi/onto/yso/p205606":"POINT(30.743444 46.485722)", +"http://www.yso.fi/onto/yso/p205608":"POINT(-16.639166666667 28.270833333333)", +"http://www.yso.fi/onto/yso/p205616":"POINT(25.59248 60.42034)", +"http://www.yso.fi/onto/yso/p205617":"POINT(-73.990277777778 40.692777777778)", +"http://www.yso.fi/onto/yso/p205678":"POINT(14.566666666667 53.533333333333)", +"http://www.yso.fi/onto/yso/p205815":"", +"http://www.yso.fi/onto/yso/p205944":"POINT(24.83375 60.22417)", +"http://www.yso.fi/onto/yso/p205961":"POINT(24.97602 60.38525)", +"http://www.yso.fi/onto/yso/p206020":"POINT(-1.98 43.32)", +"http://www.yso.fi/onto/yso/p206206":"POINT(25.27346 60.21194)", +"http://www.yso.fi/onto/yso/p206236":"POINT(24.83477 67.21081)", +"http://www.yso.fi/onto/yso/p206237":"POINT(20.1989 60.07472)", +"http://www.yso.fi/onto/yso/p206519":"POINT(25.67685 61.67484)", +"http://www.yso.fi/onto/yso/p206520":"POINT(21.66084 61.17633)", +"http://www.yso.fi/onto/yso/p206522":"POINT(-4.9 50.3)", +"http://www.yso.fi/onto/yso/p206523":"POINT(-3.8 50.7)", +"http://www.yso.fi/onto/yso/p206525":"POINT(13.008055555556 52.361944444444)", +"http://www.yso.fi/onto/yso/p206540":"POINT(111.66305555556 40.815)", +"http://www.yso.fi/onto/yso/p206779":"POINT(24.41634 66.08425)", +"http://www.yso.fi/onto/yso/p206956":"POINT(25.92456 62.34843)", +"http://www.yso.fi/onto/yso/p207219":"POINT(25.52417 65.0253)", +"http://www.yso.fi/onto/yso/p207387":"POINT(27.44748 63.30744)", +"http://www.yso.fi/onto/yso/p207534":"POINT(22.87395 62.78739)", +"http://www.yso.fi/onto/yso/p207555":"POINT(25.55 58.883333333333)", +"http://www.yso.fi/onto/yso/p207556":"POINT(24.75 58.916666666667)", +"http://www.yso.fi/onto/yso/p207557":"POINT(14.6667 63.1833)", +"http://www.yso.fi/onto/yso/p207558":"POINT(21.26474 69.30821)", +"http://www.yso.fi/onto/yso/p207559":"POINT(20.22277778 69.00888889)", +"http://www.yso.fi/onto/yso/p207560":"POINT(25.20687 65.80823)", +"http://www.yso.fi/onto/yso/p207561":"POINT(25.12535 65.83276)", +"http://www.yso.fi/onto/yso/p207563":"", +"http://www.yso.fi/onto/yso/p207571":"POINT(24.34668 59.92022)", +"http://www.yso.fi/onto/yso/p207589":"POINT(-0.13388888888889 50.824166666667)", +"http://www.yso.fi/onto/yso/p207687":"POINT(27.63726 61.12284)", +"http://www.yso.fi/onto/yso/p207689":"POINT(27.6583 61.15511)", +"http://www.yso.fi/onto/yso/p207690":"POINT(27.61018 61.12928)", +"http://www.yso.fi/onto/yso/p207691":"POINT(27.69117 61.10117)", +"http://www.yso.fi/onto/yso/p207692":"POINT(27.69243 61.10607)", +"http://www.yso.fi/onto/yso/p207693":"POINT(27.61074 61.14368)", +"http://www.yso.fi/onto/yso/p207694":"POINT(23.95632 60.86791)", +"http://www.yso.fi/onto/yso/p207696":"POINT(23.99851 60.91164)", +"http://www.yso.fi/onto/yso/p207720":"POINT(19.1329 60.30075)", +"http://www.yso.fi/onto/yso/p207867":"POINT(24.16544 60.24785)", +"http://www.yso.fi/onto/yso/p207946":"POINT(22.27483 60.42598)", +"http://www.yso.fi/onto/yso/p208004":"POINT(23.59209 63.21782)", +"http://www.yso.fi/onto/yso/p208084":"POINT(-79 37.5)", +"http://www.yso.fi/onto/yso/p208172":"POINT(-62.833333 17.916667)", +"http://www.yso.fi/onto/yso/p208412":"POINT(24.37495 60.9843)", +"http://www.yso.fi/onto/yso/p208450":"POINT(26.26249 65.03107)", +"http://www.yso.fi/onto/yso/p208556":"POINT(26.60047 61.52769)", +"http://www.yso.fi/onto/yso/p208734":"POINT(90.366666666667 23.7)", +"http://www.yso.fi/onto/yso/p208735":"POINT(67.01 24.86)", +"http://www.yso.fi/onto/yso/p208736":"POINT(73.066666666667 33.716666666667)", +"http://www.yso.fi/onto/yso/p208737":"POINT(74.343611111111 31.549722222222)", +"http://www.yso.fi/onto/yso/p208738":"POINT(113.78111111 3.03805556)", +"http://www.yso.fi/onto/yso/p208739":"POINT(105.75 21)", +"http://www.yso.fi/onto/yso/p208746":"POINT(88 15)", +"http://www.yso.fi/onto/yso/p208747":"POINT(89.633333333333 27.483333333333)", +"http://www.yso.fi/onto/yso/p208909":"POINT(125 7.5)", +"http://www.yso.fi/onto/yso/p209033":"POINT(23.59512 61.46417)", +"http://www.yso.fi/onto/yso/p209155":"POINT(22.89972222 40.75)", +"http://www.yso.fi/onto/yso/p209158":"", +"http://www.yso.fi/onto/yso/p209299":"POINT(22.416031 62.948577)", +"http://www.yso.fi/onto/yso/p209301":"POINT(22.17691218 63.15254425)", +"http://www.yso.fi/onto/yso/p209346":"POINT(27.1276 65.91669)", +"http://www.yso.fi/onto/yso/p209355":"POINT(27.72487 62.98752)", +"http://www.yso.fi/onto/yso/p209599":"POINT(25.00468 60.31334)", +"http://www.yso.fi/onto/yso/p209604":"POINT(24.19228 61.42153)", +"http://www.yso.fi/onto/yso/p209624":"POINT(21.383333333333 65.283333333333)", +"http://www.yso.fi/onto/yso/p209656":"POINT(26.18393 66.03104)", +"http://www.yso.fi/onto/yso/p209693":"POINT(26.16851 61.95428)", +"http://www.yso.fi/onto/yso/p209751":"POINT(26.57645 60.94097)", +"http://www.yso.fi/onto/yso/p209753":"POINT(22.93518 68.45972)", +"http://www.yso.fi/onto/yso/p209754":"POINT(23.40525 64.09046667)", +"http://www.yso.fi/onto/yso/p209894":"POINT(-123.20861111111 39.148888888889)", +"http://www.yso.fi/onto/yso/p209895":"", +"http://www.yso.fi/onto/yso/p209898":"POINT(23.02066 61.84507)", +"http://www.yso.fi/onto/yso/p209899":"POINT(22.83309 61.76741)", +"http://www.yso.fi/onto/yso/p209900":"POINT(23.00864 61.83754)", +"http://www.yso.fi/onto/yso/p209901":"POINT(23.15871 61.70865)", +"http://www.yso.fi/onto/yso/p209902":"POINT(-74.5 40)", +"http://www.yso.fi/onto/yso/p209903":"POINT(-1.1491666666667 52.955)", +"http://www.yso.fi/onto/yso/p209904":"POINT(10.4 63.44)", +"http://www.yso.fi/onto/yso/p209905":"POINT(-86 36)", +"http://www.yso.fi/onto/yso/p209906":"POINT(-179.4 71.233333333333)", +"http://www.yso.fi/onto/yso/p209907":"POINT(-69.966666666667 12.5)", +"http://www.yso.fi/onto/yso/p209908":"POINT(12.4852 41.8922)", +"http://www.yso.fi/onto/yso/p209919":"POINT(24.35613 61.38972)", +"http://www.yso.fi/onto/yso/p209920":"POINT(24.31628 61.41867)", +"http://www.yso.fi/onto/yso/p209921":"POINT(24.395 61.39945)", +"http://www.yso.fi/onto/yso/p209962":"POINT(27.56903 63.32862)", +"http://www.yso.fi/onto/yso/p210032":"POINT(102 0)", +"http://www.yso.fi/onto/yso/p210033":"POINT(121 -2)", +"http://www.yso.fi/onto/yso/p210063":"POINT(25.07731 60.43904)", +"http://www.yso.fi/onto/yso/p210100":"POINT(25.62222 60.42417)", +"http://www.yso.fi/onto/yso/p210105":"POINT(27.69531 63.14875)", +"http://www.yso.fi/onto/yso/p210115":"POINT(22.23324 61.61775)", +"http://www.yso.fi/onto/yso/p210139":"POINT(22.11813 63.20321)", +"http://www.yso.fi/onto/yso/p210165":"POINT(21.29319 61.97775)", +"http://www.yso.fi/onto/yso/p210214":"POINT(23.74878 60.30521)", +"http://www.yso.fi/onto/yso/p210300":"POINT(21.51213 61.07474)", +"http://www.yso.fi/onto/yso/p210314":"POINT(33.1108 63.8761)", +"http://www.yso.fi/onto/yso/p210418":"POINT(23.85641 61.47631)", +"http://www.yso.fi/onto/yso/p210565":"POINT(22.32408 59.81111)", +"http://www.yso.fi/onto/yso/p210566":"POINT(22.95179 62.95681)", +"http://www.yso.fi/onto/yso/p210600":"POINT(-83.0475 42.331666666667)", +"http://www.yso.fi/onto/yso/p210601":"POINT(37.15 36.2)", +"http://www.yso.fi/onto/yso/p210710":"POINT(26.6259 63.44739)", +"http://www.yso.fi/onto/yso/p210711":"POINT(26.66848 63.43791)", +"http://www.yso.fi/onto/yso/p210730":"POINT(13.95 62.25)", +"http://www.yso.fi/onto/yso/p210731":"POINT(25.17659 61.90332)", +"http://www.yso.fi/onto/yso/p210732":"POINT(25.1748 61.89286)", +"http://www.yso.fi/onto/yso/p210737":"POINT(19.85000001 45.25000001)", +"http://www.yso.fi/onto/yso/p210817":"", +"http://www.yso.fi/onto/yso/p210885":"POINT(23.82447 61.56796)", +"http://www.yso.fi/onto/yso/p210899":"POINT(26.29519 64.57287)", +"http://www.yso.fi/onto/yso/p210904":"POINT(21.77372 61.11959)", +"http://www.yso.fi/onto/yso/p210906":"POINT(25.76024 62.24909)", +"http://www.yso.fi/onto/yso/p210912":"POINT(26.4 40.2)", +"http://www.yso.fi/onto/yso/p210913":"POINT(29.075277777778 41.119444444444)", +"http://www.yso.fi/onto/yso/p210914":"POINT(135 40)", +"http://www.yso.fi/onto/yso/p210915":"POINT(123 35)", +"http://www.yso.fi/onto/yso/p210929":"POINT(19.93333333 50.05)", +"http://www.yso.fi/onto/yso/p210934":"POINT(24.99141 60.29132)", +"http://www.yso.fi/onto/yso/p211152":"POINT(23.79541 61.32304)", +"http://www.yso.fi/onto/yso/p211153":"POINT(23.80527 61.31379)", +"http://www.yso.fi/onto/yso/p211179":"POINT(21.91511 62.7177)", +"http://www.yso.fi/onto/yso/p211190":"POINT(-63 45)", +"http://www.yso.fi/onto/yso/p211191":"POINT(-66 46)", +"http://www.yso.fi/onto/yso/p211245":"POINT(23.614 60.66223)", +"http://www.yso.fi/onto/yso/p211246":"POINT(11.971944444444 51.483055555556)", +"http://www.yso.fi/onto/yso/p211286":"POINT(21.63202 61.22381)", +"http://www.yso.fi/onto/yso/p211287":"POINT(22.71629 61.81126)", +"http://www.yso.fi/onto/yso/p211288":"POINT(26.5246 62.60328)", +"http://www.yso.fi/onto/yso/p211289":"POINT(25.19895 62.22879)", +"http://www.yso.fi/onto/yso/p211290":"POINT(23.91537 64.27662)", +"http://www.yso.fi/onto/yso/p211294":"POINT(21.78471 62.21432)", +"http://www.yso.fi/onto/yso/p211298":"POINT(23.85755 63.00356)", +"http://www.yso.fi/onto/yso/p211490":"POINT(25.97716 62.88232)", +"http://www.yso.fi/onto/yso/p211513":"POINT(26.61846 60.86683)", +"http://www.yso.fi/onto/yso/p211514":"POINT(26.61911 60.88632)", +"http://www.yso.fi/onto/yso/p211522":"POINT(25.22025 64.83989)", +"http://www.yso.fi/onto/yso/p211537":"POINT(26.90698 60.5353)", +"http://www.yso.fi/onto/yso/p211597":"POINT(27.22766 63.78317)", +"http://www.yso.fi/onto/yso/p211598":"POINT(24.87378 61.08906)", +"http://www.yso.fi/onto/yso/p211599":"POINT(24.87391 61.10408)", +"http://www.yso.fi/onto/yso/p211600":"POINT(24.77869 61.07391)", +"http://www.yso.fi/onto/yso/p211601":"POINT(24.88762 61.10762)", +"http://www.yso.fi/onto/yso/p211602":"POINT(24.83647 61.07712)", +"http://www.yso.fi/onto/yso/p211616":"POINT(26.71392 63.27584)", +"http://www.yso.fi/onto/yso/p211628":"POINT(25.78174 64.9133)", +"http://www.yso.fi/onto/yso/p211742":"POINT(30.48691 62.25484)", +"http://www.yso.fi/onto/yso/p211758":"POINT(23.24279 60.28682)", +"http://www.yso.fi/onto/yso/p211765":"POINT(25.1541 63.8611)", +"http://www.yso.fi/onto/yso/p211766":"POINT(27.49295 63.7057)", +"http://www.yso.fi/onto/yso/p211767":"POINT(26.53946 63.29166)", +"http://www.yso.fi/onto/yso/p211773":"POINT(25.92485 62.66697)", +"http://www.yso.fi/onto/yso/p211778":"POINT(27.63133 62.92619)", +"http://www.yso.fi/onto/yso/p211861":"POINT(22.43931 63.06042)", +"http://www.yso.fi/onto/yso/p211863":"POINT(22.05226 62.93264)", +"http://www.yso.fi/onto/yso/p211864":"POINT(22.05665 62.91847)", +"http://www.yso.fi/onto/yso/p211865":"POINT(21.96571 62.89227)", +"http://www.yso.fi/onto/yso/p211869":"POINT(-8.6 41.15)", +"http://www.yso.fi/onto/yso/p211870":"POINT(-7.74888889 41.29972222)", +"http://www.yso.fi/onto/yso/p211923":"POINT(25.25152 61.60499)", +"http://www.yso.fi/onto/yso/p212010":"POINT(22.66593 62.83)", +"http://www.yso.fi/onto/yso/p212011":"POINT(22.74468 62.84467)", +"http://www.yso.fi/onto/yso/p212018":"POINT(24.72096 64.03281)", +"http://www.yso.fi/onto/yso/p212019":"POINT(24.73136 64.04)", +"http://www.yso.fi/onto/yso/p212035":"POINT(24.57845 65.71457)", +"http://www.yso.fi/onto/yso/p212041":"POINT(-107.5 43)", +"http://www.yso.fi/onto/yso/p212043":"POINT(24.9568 60.1703)", +"http://www.yso.fi/onto/yso/p212061":"POINT(26.16938 60.62822)", +"http://www.yso.fi/onto/yso/p212063":"POINT(23.64236 63.13511)", +"http://www.yso.fi/onto/yso/p212064":"POINT(23.48831 63.41493)", +"http://www.yso.fi/onto/yso/p212065":"POINT(25.1794 62.71637)", +"http://www.yso.fi/onto/yso/p212066":"POINT(29.53329 63.12848)", +"http://www.yso.fi/onto/yso/p212067":"POINT(28.38449 65.2916)", +"http://www.yso.fi/onto/yso/p212068":"POINT(24.60325 60.33038)", +"http://www.yso.fi/onto/yso/p212069":"POINT(23.73366 60.9729)", +"http://www.yso.fi/onto/yso/p212070":"POINT(22.74921 60.60342)", +"http://www.yso.fi/onto/yso/p212071":"POINT(23.35625 63.89883)", +"http://www.yso.fi/onto/yso/p212072":"POINT(23.25506 60.15733)", +"http://www.yso.fi/onto/yso/p212073":"POINT(26.54704 65.91998)", +"http://www.yso.fi/onto/yso/p212074":"POINT(25.59712 60.54237)", +"http://www.yso.fi/onto/yso/p212075":"POINT(24.6132 65.73864)", +"http://www.yso.fi/onto/yso/p212076":"POINT(22.37483 61.40996)", +"http://www.yso.fi/onto/yso/p212077":"POINT(23.73847 61.4307)", +"http://www.yso.fi/onto/yso/p212078":"POINT(25.38627 60.60065)", +"http://www.yso.fi/onto/yso/p212079":"POINT(26.68213 62.68621)", +"http://www.yso.fi/onto/yso/p212080":"POINT(24.66538 60.51059)", +"http://www.yso.fi/onto/yso/p212081":"POINT(24.67764 60.68274)", +"http://www.yso.fi/onto/yso/p212093":"POINT(19.9874 60.23583)", +"http://www.yso.fi/onto/yso/p212112":"POINT(29.29596 66.20344)", +"http://www.yso.fi/onto/yso/p212121":"POINT(24.80379 60.27871)", +"http://www.yso.fi/onto/yso/p212122":"POINT(25.08418 60.29331)", +"http://www.yso.fi/onto/yso/p212123":"POINT(24.7769 60.2825)", +"http://www.yso.fi/onto/yso/p212124":"POINT(25.02848 60.32563)", +"http://www.yso.fi/onto/yso/p212125":"POINT(25.11521 60.29497)", +"http://www.yso.fi/onto/yso/p212126":"POINT(25.11915 60.35)", +"http://www.yso.fi/onto/yso/p212127":"POINT(24.86871 60.26766)", +"http://www.yso.fi/onto/yso/p212128":"POINT(25.05262 60.28299)", +"http://www.yso.fi/onto/yso/p212129":"POINT(25.12546 60.30641)", +"http://www.yso.fi/onto/yso/p212130":"POINT(25.09563 60.36261)", +"http://www.yso.fi/onto/yso/p212131":"POINT(24.79803 60.34849)", +"http://www.yso.fi/onto/yso/p212132":"POINT(25.0851 60.33969)", +"http://www.yso.fi/onto/yso/p212133":"POINT(25.08839 60.34837)", +"http://www.yso.fi/onto/yso/p212134":"POINT(25.10093 60.34077)", +"http://www.yso.fi/onto/yso/p212135":"POINT(24.84637 60.2954)", +"http://www.yso.fi/onto/yso/p212136":"POINT(25.13718 60.33903)", +"http://www.yso.fi/onto/yso/p212137":"POINT(24.85473 60.30746)", +"http://www.yso.fi/onto/yso/p212139":"POINT(24.95888 60.27429)", +"http://www.yso.fi/onto/yso/p212140":"POINT(24.87762 60.29001)", +"http://www.yso.fi/onto/yso/p212141":"POINT(24.77145 60.32987)", +"http://www.yso.fi/onto/yso/p212142":"POINT(25.01009 60.29139)", +"http://www.yso.fi/onto/yso/p212143":"POINT(25.04309 60.35908)", +"http://www.yso.fi/onto/yso/p212144":"POINT(24.87021 60.29963)", +"http://www.yso.fi/onto/yso/p212202":"POINT(23.41159 62.86522)", +"http://www.yso.fi/onto/yso/p212205":"POINT(22.68261 61.14514)", +"http://www.yso.fi/onto/yso/p212216":"POINT(28.83098 61.22546)", +"http://www.yso.fi/onto/yso/p212217":"POINT(24.66646 63.89998)", +"http://www.yso.fi/onto/yso/p212253":"POINT(28.89145 61.24371)", +"http://www.yso.fi/onto/yso/p212263":"POINT(27.17733 64.91746)", +"http://www.yso.fi/onto/yso/p212264":"POINT(25.88054 65.71981)", +"http://www.yso.fi/onto/yso/p212265":"POINT(23.7936 61.44695)", +"http://www.yso.fi/onto/yso/p212266":"POINT(21.55483 60.875)", +"http://www.yso.fi/onto/yso/p212267":"POINT(28.13528 64.95796)", +"http://www.yso.fi/onto/yso/p212296":"POINT(27.69559 63.06206)", +"http://www.yso.fi/onto/yso/p212349":"POINT(27.72834 62.05609)", +"http://www.yso.fi/onto/yso/p212377":"POINT(35.785556 35.601944)", +"http://www.yso.fi/onto/yso/p212419":"POINT(27.6012 62.09226)", +"http://www.yso.fi/onto/yso/p212484":"POINT(32.866666666667 39.866666666667)", +"http://www.yso.fi/onto/yso/p212485":"POINT(30.683333333333 36.9)", +"http://www.yso.fi/onto/yso/p212486":"POINT(31.995 36.545)", +"http://www.yso.fi/onto/yso/p212487":"POINT(27.15 38.433333333333)", +"http://www.yso.fi/onto/yso/p212511":"POINT(3 39.5)", +"http://www.yso.fi/onto/yso/p212526":"POINT(28.20903 64.51006)", +"http://www.yso.fi/onto/yso/p212542":"POINT(28.10361 65.37141)", +"http://www.yso.fi/onto/yso/p212544":"POINT(30.07813 62.34228)", +"http://www.yso.fi/onto/yso/p212546":"POINT(27.63894 62.52993)", +"http://www.yso.fi/onto/yso/p212549":"POINT(23.90913 60.58576)", +"http://www.yso.fi/onto/yso/p212550":"POINT(29.04235 61.81417)", +"http://www.yso.fi/onto/yso/p212551":"POINT(30.06315 62.21943)", +"http://www.yso.fi/onto/yso/p212552":"POINT(23.10598 61.69699)", +"http://www.yso.fi/onto/yso/p212553":"POINT(28.04023 61.81199)", +"http://www.yso.fi/onto/yso/p212554":"POINT(25.62172 61.27079)", +"http://www.yso.fi/onto/yso/p212555":"POINT(26.82818 67.35687)", +"http://www.yso.fi/onto/yso/p212556":"POINT(24.58683 60.30808)", +"http://www.yso.fi/onto/yso/p212557":"POINT(23.65071 61.46163)", +"http://www.yso.fi/onto/yso/p212559":"POINT(24.93228 66.76954)", +"http://www.yso.fi/onto/yso/p212561":"POINT(25.69731 60.97714)", +"http://www.yso.fi/onto/yso/p212564":"POINT(25.34956 60.55381)", +"http://www.yso.fi/onto/yso/p212565":"POINT(24.91223 62.33423)", +"http://www.yso.fi/onto/yso/p212566":"POINT(24.56488 62.29326)", +"http://www.yso.fi/onto/yso/p212567":"POINT(23.52633 63.64938)", +"http://www.yso.fi/onto/yso/p212568":"POINT(25.09392 60.48308)", +"http://www.yso.fi/onto/yso/p212569":"POINT(23.91324 60.67151)", +"http://www.yso.fi/onto/yso/p212570":"POINT(27.16043 63.05666)", +"http://www.yso.fi/onto/yso/p212573":"POINT(21.66134 60.78258)", +"http://www.yso.fi/onto/yso/p212574":"POINT(29.31975 61.78502)", +"http://www.yso.fi/onto/yso/p212677":"POINT(21.23686 62.39806)", +"http://www.yso.fi/onto/yso/p212684":"POINT(27.14484 62.71104)", +"http://www.yso.fi/onto/yso/p212695":"POINT(25.25525 62.14109)", +"http://www.yso.fi/onto/yso/p212781":"POINT(-2.1 57.15)", +"http://www.yso.fi/onto/yso/p212862":"POINT(24.95121 60.28867)", +"http://www.yso.fi/onto/yso/p212863":"", +"http://www.yso.fi/onto/yso/p212863":"", +"http://www.yso.fi/onto/yso/p212864":"", +"http://www.yso.fi/onto/yso/p212864":"", +"http://www.yso.fi/onto/yso/p212865":"", +"http://www.yso.fi/onto/yso/p212865":"POINT(25.125 60.26833333)", +"http://www.yso.fi/onto/yso/p212866":"", +"http://www.yso.fi/onto/yso/p212866":"POINT(24.9639 60.2972)", +"http://www.yso.fi/onto/yso/p212867":"POINT(25.07239 60.36413)", +"http://www.yso.fi/onto/yso/p212947":"POINT(28.194916944444 61.056528055556)", +"http://www.yso.fi/onto/yso/p212981":"POINT(-71.061666666667 42.357777777778)", +"http://www.yso.fi/onto/yso/p212982":"POINT(16.733333333333 59.133333333333)", +"http://www.yso.fi/onto/yso/p212983":"POINT(19 69.666666666667)", +"http://www.yso.fi/onto/yso/p212984":"POINT(13 43.316666666667)", +"http://www.yso.fi/onto/yso/p212985":"POINT(-86.7 32.7)", +"http://www.yso.fi/onto/yso/p212991":"POINT(24.41172 60.31904)", +"http://www.yso.fi/onto/yso/p212992":"POINT(27.38924 61.18656)", +"http://www.yso.fi/onto/yso/p213060":"POINT(22.28248 60.42156)", +"http://www.yso.fi/onto/yso/p213061":"POINT(22.30943 60.45207)", +"http://www.yso.fi/onto/yso/p213062":"POINT(22.27632 60.47153)", +"http://www.yso.fi/onto/yso/p213063":"POINT(22.23897 60.39932)", +"http://www.yso.fi/onto/yso/p213064":"POINT(22.21697 60.39125)", +"http://www.yso.fi/onto/yso/p213065":"POINT(22.32963 60.42653)", +"http://www.yso.fi/onto/yso/p213066":"POINT(22.19077 60.41141)", +"http://www.yso.fi/onto/yso/p213067":"POINT(22.22025 60.44972)", +"http://www.yso.fi/onto/yso/p213068":"POINT(22.35251 60.54144)", +"http://www.yso.fi/onto/yso/p213069":"POINT(22.1846 60.41981)", +"http://www.yso.fi/onto/yso/p213070":"POINT(22.2653 60.47922)", +"http://www.yso.fi/onto/yso/p213071":"POINT(22.25061 60.40369)", +"http://www.yso.fi/onto/yso/p213072":"POINT(22.34126 60.46106)", +"http://www.yso.fi/onto/yso/p213073":"POINT(22.23235 60.43206)", +"http://www.yso.fi/onto/yso/p213075":"POINT(22.32688 60.54919)", +"http://www.yso.fi/onto/yso/p213076":"POINT(22.22798 60.41184)", +"http://www.yso.fi/onto/yso/p213077":"POINT(22.21099 60.42462)", +"http://www.yso.fi/onto/yso/p213078":"POINT(22.15818 60.40676)", +"http://www.yso.fi/onto/yso/p213079":"POINT(22.23477 60.41672)", +"http://www.yso.fi/onto/yso/p213080":"POINT(22.30079 60.53261)", +"http://www.yso.fi/onto/yso/p213081":"POINT(22.22262 60.47601)", +"http://www.yso.fi/onto/yso/p213096":"POINT(22.31816 60.48101)", +"http://www.yso.fi/onto/yso/p213097":"POINT(22.16373 60.39655)", +"http://www.yso.fi/onto/yso/p213098":"POINT(22.30053 60.42449)", +"http://www.yso.fi/onto/yso/p213292":"POINT(24.44458 66.3977)", +"http://www.yso.fi/onto/yso/p213315":"POINT(21.73 69.016666666667)", +"http://www.yso.fi/onto/yso/p213341":"", +"http://www.yso.fi/onto/yso/p213343":"POINT(22.81107 62.94298)", +"http://www.yso.fi/onto/yso/p213344":"", +"http://www.yso.fi/onto/yso/p213345":"", +"http://www.yso.fi/onto/yso/p213346":"", +"http://www.yso.fi/onto/yso/p213347":"", +"http://www.yso.fi/onto/yso/p213348":"", +"http://www.yso.fi/onto/yso/p213349":"", +"http://www.yso.fi/onto/yso/p213352":"", +"http://www.yso.fi/onto/yso/p213353":"", +"http://www.yso.fi/onto/yso/p213354":"", +"http://www.yso.fi/onto/yso/p213355":"", +"http://www.yso.fi/onto/yso/p213356":"", +"http://www.yso.fi/onto/yso/p213358":"", +"http://www.yso.fi/onto/yso/p213436":"POINT(34.616666666667 57.15)", +"http://www.yso.fi/onto/yso/p213485":"POINT(-0.68333333333333 37.977777777778)", +"http://www.yso.fi/onto/yso/p213869":"", +"http://www.yso.fi/onto/yso/p213944":"POINT(28.7677 61.17217)", +"http://www.yso.fi/onto/yso/p213962":"POINT(22.34776 61.26051)", +"http://www.yso.fi/onto/yso/p214103":"POINT(109.33333333333 53.8)", +"http://www.yso.fi/onto/yso/p214105":"POINT(23.73506 61.2832)", +"http://www.yso.fi/onto/yso/p214175":"POINT(25.06175 60.48598)", +"http://www.yso.fi/onto/yso/p214279":"POINT(22.36026 60.43423)", +"http://www.yso.fi/onto/yso/p214365":"POINT(25.05701 64.14996)", +"http://www.yso.fi/onto/yso/p214635":"POINT(24.50423 61.00659)", +"http://www.yso.fi/onto/yso/p215096":"POINT(25.3706 65.34397)", +"http://www.yso.fi/onto/yso/p215142":"POINT(30.85215 62.61788)", +"http://www.yso.fi/onto/yso/p215143":"POINT(31.12209 62.80185)", +"http://www.yso.fi/onto/yso/p215144":"POINT(23.52187 63.60992)", +"http://www.yso.fi/onto/yso/p215155":"POINT(22.24144 68.53553)", +"http://www.yso.fi/onto/yso/p215156":"POINT(22.39489 68.52989)", +"http://www.yso.fi/onto/yso/p215187":"POINT(23.15856 63.87834)", +"http://www.yso.fi/onto/yso/p215197":"POINT(23.59925 60.2594)", +"http://www.yso.fi/onto/yso/p215318":"POINT(26.35548 60.86799)", +"http://www.yso.fi/onto/yso/p215388":"POINT(27.44395 64.12395)", +"http://www.yso.fi/onto/yso/p215500":"POINT(26.3892 61.76639)", +"http://www.yso.fi/onto/yso/p215501":"POINT(26.44222 61.74407)", +"http://www.yso.fi/onto/yso/p215503":"POINT(26.44222 61.74407)", +"http://www.yso.fi/onto/yso/p215508":"POINT(26.34632 61.18842)", +"http://www.yso.fi/onto/yso/p215587":"POINT(24.89828 67.48143)", +"http://www.yso.fi/onto/yso/p215599":"POINT(30.73475 62.24533)", +"http://www.yso.fi/onto/yso/p504574":"POINT(10.898333333333 48.371666666667)", +"http://www.yso.fi/onto/yso/p504575":"POINT(13.033333333333 47.8)", +"http://www.yso.fi/onto/yso/p504798":"POINT(88 24)", +"http://www.yso.fi/onto/yso/p504973":"POINT(14.666666666667 63.183333333333)", +"http://www.yso.fi/onto/yso/p505012":"", +"http://www.yso.fi/onto/yso/p505080":"POINT(-2 51.2)", +"http://www.yso.fi/onto/yso/p505105":"POINT(101.5 24.5)", +"http://www.yso.fi/onto/yso/p505172":"POINT(47.81 30.488888888889)", +"http://www.yso.fi/onto/yso/p94080":"POINT(17 1)", +"http://www.yso.fi/onto/yso/p94081":"POINT(19.94234 60.1014)", +"http://www.yso.fi/onto/yso/p94083":"POINT(22.8484 63.24722)", +"http://www.yso.fi/onto/yso/p94084":"POINT(23.81752 63.00127)", +"http://www.yso.fi/onto/yso/p94085":"POINT(22.86383 60.95165)", +"http://www.yso.fi/onto/yso/p94086":"POINT(23.6166 62.58616)", +"http://www.yso.fi/onto/yso/p94090":"POINT(26.823611111111 60.75)", +"http://www.yso.fi/onto/yso/p94093":"POINT(22.23942 60.43678)", +"http://www.yso.fi/onto/yso/p94095":"", +"http://www.yso.fi/onto/yso/p94096":"POINT(23.0339 59.98874)", +"http://www.yso.fi/onto/yso/p94101":"POINT(22.46998 60.07397)", +"http://www.yso.fi/onto/yso/p94104":"POINT(23.63531 68.38555)", +"http://www.yso.fi/onto/yso/p94105":"POINT(24.65673 60.20638)", +"http://www.yso.fi/onto/yso/p94106":"POINT(28.17183 61.05435)", +"http://www.yso.fi/onto/yso/p94107":"POINT(22.84231 62.78651)", +"http://www.yso.fi/onto/yso/p94108":"POINT(22.13593 61.13)", +"http://www.yso.fi/onto/yso/p94109":"POINT(21.7341 61.20355)", +"http://www.yso.fi/onto/yso/p94111":"POINT(7 46)", +"http://www.yso.fi/onto/yso/p94112":"POINT(23.47395 63.3669)", +"http://www.yso.fi/onto/yso/p94113":"POINT(19.99238 60.23226)", +"http://www.yso.fi/onto/yso/p94114":"POINT(23.61641 60.81479)", +"http://www.yso.fi/onto/yso/p94119":"POINT(27.19232 60.28913)", +"http://www.yso.fi/onto/yso/p94120":"POINT(25.36441 64.13615)", +"http://www.yso.fi/onto/yso/p94121":"POINT(24.71113 65.0086)", +"http://www.yso.fi/onto/yso/p94122":"POINT(23.07384 60.39886)", +"http://www.yso.fi/onto/yso/p94123":"POINT(24.17159 63.46015)", +"http://www.yso.fi/onto/yso/p94124":"POINT(27.1979 60.56969)", +"http://www.yso.fi/onto/yso/p94125":"POINT(19.73979 60.21834)", +"http://www.yso.fi/onto/yso/p94126":"POINT(22.96684 59.82497)", +"http://www.yso.fi/onto/yso/p94127":"POINT(22.1412 61.3137)", +"http://www.yso.fi/onto/yso/p94128":"POINT(26.01945 61.58353)", +"http://www.yso.fi/onto/yso/p94129":"POINT(24.37369 61.05594)", +"http://www.yso.fi/onto/yso/p94130":"POINT(25.3512 65.1777)", +"http://www.yso.fi/onto/yso/p94131":"POINT(28.36356 62.17889)", +"http://www.yso.fi/onto/yso/p94132":"POINT(25.02474 60.78835)", +"http://www.yso.fi/onto/yso/p94133":"", +"http://www.yso.fi/onto/yso/p94134":"POINT(26.03484 61.20524)", +"http://www.yso.fi/onto/yso/p94135":"POINT(25.033333333333 60.3)", +"http://www.yso.fi/onto/yso/p94137":"POINT(24.94354 60.16664)", +"http://www.yso.fi/onto/yso/p94141":"POINT(22.52312 59.89229)", +"http://www.yso.fi/onto/yso/p94142":"POINT(25.51281 60.98861)", +"http://www.yso.fi/onto/yso/p94143":"POINT(24.86112 60.63102)", +"http://www.yso.fi/onto/yso/p94144":"POINT(24.565555555556 62.17)", +"http://www.yso.fi/onto/yso/p94145":"", +"http://www.yso.fi/onto/yso/p94147":"POINT(23.19571 61.64092)", +"http://www.yso.fi/onto/yso/p94148":"POINT(24.46442 60.99617)", +"http://www.yso.fi/onto/yso/p94150":"POINT(27.18698 63.55969)", +"http://www.yso.fi/onto/yso/p94151":"POINT(26.33869 60.89081)", +"http://www.yso.fi/onto/yso/p94152":"POINT(23.07459 61.76825)", +"http://www.yso.fi/onto/yso/p94153":"POINT(30.9268 62.67285)", +"http://www.yso.fi/onto/yso/p94154":"POINT(28.76922 61.17153)", +"http://www.yso.fi/onto/yso/p94155":"", +"http://www.yso.fi/onto/yso/p94157":"POINT(21.38728 60.39723)", +"http://www.yso.fi/onto/yso/p94158":"POINT(24.00657 60.04466)", +"http://www.yso.fi/onto/yso/p94159":"POINT(21.95848 62.11514)", +"http://www.yso.fi/onto/yso/p94162":"POINT(26.48118 61.05251)", +"http://www.yso.fi/onto/yso/p94163":"POINT(22.76297 62.49237)", +"http://www.yso.fi/onto/yso/p94164":"POINT(24.64771 60.92185)", +"http://www.yso.fi/onto/yso/p94166":"POINT(29.75968 62.60208)", +"http://www.yso.fi/onto/yso/p94167":"POINT(19.95257 60.15043)", +"http://www.yso.fi/onto/yso/p94168":"POINT(28.3297 63.06596)", +"http://www.yso.fi/onto/yso/p94171":"POINT(29.25433 63.23986)", +"http://www.yso.fi/onto/yso/p94172":"POINT(27.85734 61.89747)", +"http://www.yso.fi/onto/yso/p94173":"POINT(25.7495 62.24168)", +"http://www.yso.fi/onto/yso/p94174":"POINT(22.69102 61.82022)", +"http://www.yso.fi/onto/yso/p94175":"POINT(25.19023 61.86426)", +"http://www.yso.fi/onto/yso/p94176":"POINT(25.17436 61.91911)", +"http://www.yso.fi/onto/yso/p94177":"POINT(25.10075 60.4811)", +"http://www.yso.fi/onto/yso/p94178":"POINT(22.371 60.40762)", +"http://www.yso.fi/onto/yso/p94179":"POINT(28.48084 62.97498)", +"http://www.yso.fi/onto/yso/p94180":"POINT(27.71589 64.22164)", +"http://www.yso.fi/onto/yso/p94181":"POINT(27.72805 64.22673)", +"http://www.yso.fi/onto/yso/p94182":"POINT(23.94916 64.26043)", +"http://www.yso.fi/onto/yso/p94183":"POINT(21.56597 60.81812)", +"http://www.yso.fi/onto/yso/p94186":"POINT(24.06717 61.4632)", +"http://www.yso.fi/onto/yso/p94187":"POINT(22.39516 61.80336)", +"http://www.yso.fi/onto/yso/p94188":"POINT(29.396666666667 60.35)", +"http://www.yso.fi/onto/yso/p94189":"POINT(25.26343 62.97657)", +"http://www.yso.fi/onto/yso/p94190":"POINT(21.7049 62.30842)", +"http://www.yso.fi/onto/yso/p94191":"POINT(23.66932 60.06322)", +"http://www.yso.fi/onto/yso/p94192":"POINT(32 63)", +"http://www.yso.fi/onto/yso/p94193":"POINT(24.20821 60.53493)", +"http://www.yso.fi/onto/yso/p94194":"POINT(24.8005 62.8766)", +"http://www.yso.fi/onto/yso/p94195":"POINT(26.97046 62.8963)", +"http://www.yso.fi/onto/yso/p94196":"POINT(22.56256 62.13854)", +"http://www.yso.fi/onto/yso/p94197":"POINT(22.17724 62.42509)", +"http://www.yso.fi/onto/yso/p94199":"POINT(24.72775 60.21064)", +"http://www.yso.fi/onto/yso/p94200":"POINT(26.34995 63.17959)", +"http://www.yso.fi/onto/yso/p94202":"POINT(24.56342 65.73364)", +"http://www.yso.fi/onto/yso/p94203":"POINT(27.43058 66.71502)", +"http://www.yso.fi/onto/yso/p94204":"POINT(22.73765 60.16795)", +"http://www.yso.fi/onto/yso/p94205":"POINT(25.10355 60.40487)", +"http://www.yso.fi/onto/yso/p94206":"POINT(23.08037 63.85096)", +"http://www.yso.fi/onto/yso/p94207":"POINT(25.76877 62.2405)", +"http://www.yso.fi/onto/yso/p94208":"POINT(29.83495 61.89752)", +"http://www.yso.fi/onto/yso/p94209":"POINT(24.70645 62.26021)", +"http://www.yso.fi/onto/yso/p94210":"POINT(24.43891 60.12066)", +"http://www.yso.fi/onto/yso/p94212":"POINT(29.317067 61.067629)", +"http://www.yso.fi/onto/yso/p94213":"POINT(24.91144 67.65313)", +"http://www.yso.fi/onto/yso/p94214":"POINT(22.0845 61.21245)", +"http://www.yso.fi/onto/yso/p94215":"POINT(29.7375 60.361666666667)", +"http://www.yso.fi/onto/yso/p94216":"POINT(22.35046 61.25614)", +"http://www.yso.fi/onto/yso/p94217":"POINT(23.13196 63.83758)", +"http://www.yso.fi/onto/yso/p94218":"POINT(23.78897 67.33172)", +"http://www.yso.fi/onto/yso/p94221":"POINT(21.56476 60.16251)", +"http://www.yso.fi/onto/yso/p94222":"POINT(23.15503 63.3003)", +"http://www.yso.fi/onto/yso/p94223":"POINT(23.13737 60.65298)", +"http://www.yso.fi/onto/yso/p94224":"POINT(26.94115 60.46552)", +"http://www.yso.fi/onto/yso/p94225":"POINT(26.7056 60.86683)", +"http://www.yso.fi/onto/yso/p94227":"POINT(21.37855 62.27474)", +"http://www.yso.fi/onto/yso/p94228":"POINT(23.03377 63.72782)", +"http://www.yso.fi/onto/yso/p94229":"POINT(29.52437 64.12529)", +"http://www.yso.fi/onto/yso/p94230":"POINT(25.17567 61.56243)", +"http://www.yso.fi/onto/yso/p94232":"POINT(20.77831 60.25969)", +"http://www.yso.fi/onto/yso/p94233":"", +"http://www.yso.fi/onto/yso/p94234":"POINT(27.68893 62.89298)", +"http://www.yso.fi/onto/yso/p94235":"POINT(24.81666667 61.85)", +"http://www.yso.fi/onto/yso/p94236":"POINT(22.39877 62.61889)", +"http://www.yso.fi/onto/yso/p94237":"", +"http://www.yso.fi/onto/yso/p94238":"POINT(23.73217 61.87824)", +"http://www.yso.fi/onto/yso/p94239":"POINT(21.35888 60.54671)", +"http://www.yso.fi/onto/yso/p94240":"POINT(29.18862 65.96442)", +"http://www.yso.fi/onto/yso/p94241":"POINT(26.62428 60.90697)", +"http://www.yso.fi/onto/yso/p94242":"POINT(23.22092 60.52037)", +"http://www.yso.fi/onto/yso/p94243":"POINT(26.69029 60.8762)", +"http://www.yso.fi/onto/yso/p94244":"POINT(23.45291 63.8591)", +"http://www.yso.fi/onto/yso/p94245":"POINT(25.75967 63.98059)", +"http://www.yso.fi/onto/yso/p94250":"POINT(25.65499 60.98038)", +"http://www.yso.fi/onto/yso/p94251":"POINT(22.01117 62.97796)", +"http://www.yso.fi/onto/yso/p94252":"POINT(21.69023 60.87586)", +"http://www.yso.fi/onto/yso/p94254":"POINT(23.63285 63.2176)", +"http://www.yso.fi/onto/yso/p94255":"POINT(28.18769 61.05875)", +"http://www.yso.fi/onto/yso/p94256":"POINT(25.69819 66.49914)", +"http://www.yso.fi/onto/yso/p94257":"POINT(23.00757 62.97017)", +"http://www.yso.fi/onto/yso/p94258":"POINT(21.5212 62.24108)", +"http://www.yso.fi/onto/yso/p94259":"POINT(25 57)", +"http://www.yso.fi/onto/yso/p94260":"POINT(25.95778 62.41353)", +"http://www.yso.fi/onto/yso/p94261":"POINT(26.12796 61.91198)", +"http://www.yso.fi/onto/yso/p94262":"POINT(27.80496 61.06152)", +"http://www.yso.fi/onto/yso/p94263":"POINT(20.0996 60.06845)", +"http://www.yso.fi/onto/yso/p94264":"POINT(23.75575 61.31253)", +"http://www.yso.fi/onto/yso/p94265":"POINT(30.316666666667 59.95)", +"http://www.yso.fi/onto/yso/p94266":"POINT(30.0281 63.3178)", +"http://www.yso.fi/onto/yso/p94268":"", +"http://www.yso.fi/onto/yso/p94270":"POINT(29.36786 62.53448)", +"http://www.yso.fi/onto/yso/p94271":"POINT(24.06578 60.25092)", +"http://www.yso.fi/onto/yso/p94272":"POINT(23.50257 64.0225)", +"http://www.yso.fi/onto/yso/p94273":"POINT(23.05859 60.85136)", +"http://www.yso.fi/onto/yso/p94274":"POINT(24.44357 60.71756)", +"http://www.yso.fi/onto/yso/p94276":"", +"http://www.yso.fi/onto/yso/p94277":"POINT(26.22366 60.45773)", +"http://www.yso.fi/onto/yso/p94278":"POINT(25.18713 64.83762)", +"http://www.yso.fi/onto/yso/p94279":"POINT(22.74668 63.75549)", +"http://www.yso.fi/onto/yso/p94280":"POINT(27.55974 60.92092)", +"http://www.yso.fi/onto/yso/p94283":"POINT(21.55285 62.94291)", +"http://www.yso.fi/onto/yso/p94284":"POINT(19.94227 60.10152)", +"http://www.yso.fi/onto/yso/p94285":"POINT(22.05906 63.21564)", +"http://www.yso.fi/onto/yso/p94288":"POINT(22.89897 60.58541)", +"http://www.yso.fi/onto/yso/p94289":"POINT(27.27322 61.68773)", +"http://www.yso.fi/onto/yso/p94292":"POINT(37.617777777778 55.755833333333)", +"http://www.yso.fi/onto/yso/p94294":"POINT(25.99005 64.80892)", +"http://www.yso.fi/onto/yso/p94295":"POINT(29.566666666667 60.55)", +"http://www.yso.fi/onto/yso/p94296":"POINT(23.67722 67.95928)", +"http://www.yso.fi/onto/yso/p94297":"POINT(21.68075 63.11405)", +"http://www.yso.fi/onto/yso/p94298":"POINT(21.98751 60.67873)", +"http://www.yso.fi/onto/yso/p94301":"POINT(24.60477 62.03555)", +"http://www.yso.fi/onto/yso/p94302":"POINT(22.01822 60.46771)", +"http://www.yso.fi/onto/yso/p94303":"POINT(21.89956 60.20299)", +"http://www.yso.fi/onto/yso/p94304":"POINT(37.616666666667 55.75)", +"http://www.yso.fi/onto/yso/p94306":"POINT(28.08223 63.20746)", +"http://www.yso.fi/onto/yso/p94307":"POINT(24.95394 63.92964)", +"http://www.yso.fi/onto/yso/p94308":"POINT(23.5085 61.47877)" +} diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/__init__.py b/src/metax_api/tasks/refdata/refdata_indexer/service/__init__.py new file mode 100755 index 00000000..74447540 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/elasticsearch_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/elasticsearch_service.py new file mode 100755 index 00000000..d769e3e0 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/elasticsearch_service.py @@ -0,0 +1,111 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +import json +import logging + +from django.conf import settings +from elasticsearch import Elasticsearch + +_logger = logging.getLogger(__name__) + + +class ElasticSearchService: + """ + Service for operating with Elasticsearch APIs. Used when data indices are created/deleted and + data is deleted/reindexed. + """ + + ES_CONFIG_DIR = settings.ES_CONFIG_DIR + + REF_DATA_INDEX_NAME = "reference_data" + REF_DATA_INDEX_FILENAME = ES_CONFIG_DIR + "reference_data_index.json" + + ORG_DATA_INDEX_NAME = "organization_data" + ORG_DATA_INDEX_FILENAME = ES_CONFIG_DIR + "organization_data_index.json" + + def __init__(self): + self.es = Elasticsearch(settings.ELASTICSEARCH["HOSTS"]) + + def index_exists(self, index): + return self.es.indices.exists(index=index) + + def create_index(self, index, filename): + _logger.info("Trying to create index " + index) + return self._operation_ok( + self.es.indices.create( + index=index, body=self._get_json_file_as_str(filename) + ) + ) + + def delete_index(self, index): + _logger.info("Trying to delete index " + index) + return self._operation_ok(self.es.indices.delete(index=index, ignore=[404])) + + def delete_and_update_indexable_data(self, index, doc_type, indexable_data_list): + if len(indexable_data_list) > 0: + bulk_update_str = "\n".join( + map( + lambda idx_data: self._create_bulk_update_row_for_indexable_data( + index, idx_data + ), + indexable_data_list, + ) + ) + self._delete_all_documents_from_index_with_type(index, doc_type) + _logger.info( + "Trying to bulk update reference data with type " + + doc_type + + " to index " + + index + ) + + return self._operation_ok( + self.es.bulk(body=bulk_update_str, request_timeout=30) + ) + + return None + + def _delete_all_documents_from_index_with_type(self, index, doc_type): + _logger.info( + "Trying to delete all documents from index " + + index + + " having type " + + doc_type + ) + return self._operation_ok( + self.es.delete_by_query( + index=index, + request_timeout=30, + body='{"query": { "match": {"type": "%s"}}}' % doc_type, + ) + ) + + def _create_bulk_update_row_for_indexable_data(self, index, indexable_data_item): + return ( + '{"index":{"_index": "' + + index + + '", "_id":"' + + indexable_data_item.get_es_document_id() + + '"}}\n' + + indexable_data_item.to_es_document() + ) + + def _create_bulk_delete_row_indexable_data(self, index, indexable_data_item): + return ( + '{"delete":{"_index": "' + + index + + '", "_id":"' + + indexable_data_item.get_es_document_id() + + '"}}' + ) + + def _operation_ok(self, op_response): + if op_response.get("acknowledged"): + _logger.info("OK") + return True + return False + + def _get_json_file_as_str(self, filename): + with open(filename) as json_data: + return json.load(json_data) diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/finto_data_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/finto_data_service.py new file mode 100755 index 00000000..1a2f0a22 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/finto_data_service.py @@ -0,0 +1,231 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +import json +import logging +from time import sleep + +import requests +from django.conf import settings +from rdflib import Graph, URIRef, RDF +from rdflib.namespace import SKOS + +from metax_api.tasks.refdata.refdata_indexer.domain.reference_data import ReferenceData + +_logger = logging.getLogger(__name__) + + +class FintoDataService: + """ + Service for getting reference data for elasticsearch index. The data is in Finto, + so it is first fetched and parsed. + """ + + FINTO_REFERENCE_DATA_SOURCE_URLS = { + ReferenceData.DATA_TYPE_FIELD_OF_SCIENCE: "http://finto.fi/rest/v1/okm-tieteenala/data", + ReferenceData.DATA_TYPE_LANGUAGE: "http://finto.fi/rest/v1/lexvo/data", + ReferenceData.DATA_TYPE_LOCATION: "http://finto.fi/rest/v1/yso-paikat/data", + ReferenceData.DATA_TYPE_KEYWORD: "http://finto.fi/rest/v1/koko/data", + } + + WKT_FILENAME = settings.WKT_FILENAME + + # Use this to decide whether to read location coordinates from a file + # or whether to read coordinates from wikidata and paikkatiedot.fi and + # at the same time writing the coordinates to a file + READ_COORDINATES_FROM_FILE = True + + def get_data(self, data_type): + graph = self._fetch_finto_data(data_type) + + if graph is None: + return [] + + index_data_models = self._parse_finto_data(graph, data_type) + return index_data_models + + def _parse_finto_data(self, graph, data_type): + index_data_models = [] + + if data_type == ReferenceData.DATA_TYPE_LOCATION: + if self.READ_COORDINATES_FROM_FILE: + with open(self.WKT_FILENAME) as c: + coordinates = json.load(c) + else: + with open(self.WKT_FILENAME, "w") as outfile: + outfile.write("{\n") + + _logger.info("Extracting relevant data from the fetched data") + + in_scheme = "" + for concept in graph.subjects(RDF.type, SKOS.Concept): + for value in graph.objects(concept, SKOS.inScheme): + in_scheme = str(value) + break + break + + for concept in graph.subjects(RDF.type, SKOS.Concept): + uri = str(concept) + # preferred labels + label = dict( + ( + (literal.language, str(literal)) + for literal in graph.objects(concept, SKOS.prefLabel) + ) + ) + # parents (broader) + parent_ids = [ + self._get_uri_end_part(parent) + for parent in graph.objects(concept, SKOS.broader) + ] + # children (narrower) + child_ids = [ + self._get_uri_end_part(child) + for child in graph.objects(concept, SKOS.narrower) + ] + same_as = [] + wkt = "" + if data_type == ReferenceData.DATA_TYPE_LOCATION: + # find out the coordinates of matching PNR or Wikidata entities + matches = sorted(graph.objects(concept, SKOS.closeMatch)) + for match in matches: + if self.READ_COORDINATES_FROM_FILE: + wkt = coordinates.get(uri, "") + else: + wkt = self._get_coordinates_for_location_from_url(match) + with open(self.WKT_FILENAME, "a") as outfile: + outfile.write('"' + uri + '":"' + wkt + '",\n') + if wkt != "": + # Stop after first success + break + + data_id = self._get_uri_end_part(concept) + + ref_item = ReferenceData( + data_id, + data_type, + label, + uri, + parent_ids=parent_ids, + child_ids=child_ids, + same_as=same_as, + wkt=wkt, + scheme=in_scheme, + ) + index_data_models.append(ref_item) + + if data_type == ReferenceData.DATA_TYPE_LOCATION: + if not self.READ_COORDINATES_FROM_FILE: + with open(self.WKT_FILENAME, "a") as outfile: + outfile.write("}") + + _logger.info("Done with all") + return index_data_models + + def _fetch_finto_data(self, data_type): + url = self.FINTO_REFERENCE_DATA_SOURCE_URLS[data_type] + _logger.info("Fetching data from url " + url) + sleep_time = 2 + num_retries = 7 + g = Graph() + + for x in range(0, num_retries): + try: + g.parse(url) + str_error = None + except Exception as e: + str_error = e + + if str_error: + sleep(sleep_time) # wait before trying to fetch the data again + sleep_time *= 2 # exponential backoff + else: + break + + if not str_error: + return g + else: + _logger.error( + "Failed to read Finto data of type %s, skipping.." % data_type + ) + return None + + def _get_uri_end_part(self, uri): + return uri[uri.rindex("/") + 1:].strip() + + def _get_coordinates_for_location_from_url(self, url): + sleep_time = 2 + num_retries = 4 + + if "wikidata" in url: + g = Graph() + for x in range(0, num_retries): + try: + g.parse(url + ".rdf") + str_error = None + except Exception as e: + str_error = e + + if str_error: + _logger.error("Unable to read wikidata, trying again..") + sleep(sleep_time) # wait before trying to fetch the data again + sleep_time *= 2 # exponential backoff + else: + break + + if not str_error: + subject = URIRef(url) + predicate = URIRef("http://www.wikidata.org/prop/direct/P625") + for o in g.objects(subject, predicate): + return str(o).upper() + else: + _logger.error("Failed to read wikidata, skipping..") + + elif "paikkatiedot" in url: + for x in range(0, num_retries): + try: + response = requests.get(url + ".jsonld") + str_error = None + except Exception as e: + str_error = e + + if str_error: + _logger.error("Unable to read paikkatiedot, trying again..") + sleep(sleep_time) # wait before trying to fetch the data again + sleep_time *= 2 # exponential backoff + else: + break + + if not str_error and response and response.status_code == requests.codes.ok: + data_as_str = self._find_between( + response.text, '" + ) + if data_as_str: + data = json.loads(data_as_str) + if ( + data + and data.get("geo", False) + and data.get("geo").get("latitude", False) + and data.get("geo").get("longitude", False) + ): + return ( + "POINT(" + + str(data["geo"]["longitude"]) + + " " + + str(data["geo"]["latitude"]) + + ")" + ) + else: + _logger.error("Failed to read pakkatiedot, skipping..") + + return "" + + def _find_between(self, s, first, last): + try: + if s: + start = s.index(first) + len(first) + end = s.index(last, start) + return s[start:end] + except Exception: + pass + return None diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/infra_data_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/infra_data_service.py new file mode 100755 index 00000000..db118904 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/infra_data_service.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +import json +import logging +import os +from time import sleep + +import requests + +from metax_api.tasks.refdata.refdata_indexer.domain.reference_data import ReferenceData +from metax_api.tasks.refdata.refdata_indexer.service.service_utils import file_exists + +_logger = logging.getLogger(__name__) + + +class InfraDataService: + """ + Service for getting research infrastructure data for elasticsearch index. The data is in AVAA, + so it is first fetched and parsed. + """ + + INFRA_REF_DATA_SOURCE_URL = "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures" + + TEMP_FILENAME = "/tmp/data.json" + + def get_data(self): + self._fetch_infra_data() + + if not file_exists(self.TEMP_FILENAME): + return [] + + index_data_models = self._parse_infra_data() + os.remove(self.TEMP_FILENAME) + + return index_data_models + + def _parse_infra_data(self): + index_data_models = [] + + _logger.info("Extracting relevant data from the fetched data") + with open(self.TEMP_FILENAME, "r") as f: + data = json.load(f) + for item in data: + if item.get("urn", ""): + data_id = self._get_data_id(item["urn"]) + data_type = ReferenceData.DATA_TYPE_RESEARCH_INFRA + uri = "http://urn.fi/" + item["urn"] + same_as = [] + label = {} + if item.get("name_FI"): + label["fi"] = item["name_FI"] + if item.get("name_EN"): + label["en"] = item["name_EN"] + + ref_item = ReferenceData( + data_id, + data_type, + label, + uri, + same_as=same_as, + scheme=InfraDataService.INFRA_REF_DATA_SOURCE_URL, + ) + index_data_models.append(ref_item) + + return index_data_models + + def _fetch_infra_data(self): + url = self.INFRA_REF_DATA_SOURCE_URL + _logger.info("Fetching data from url " + url) + + sleep_time = 2 + num_retries = 7 + + for x in range(0, num_retries): + try: + response = requests.get(url, stream=True) + str_error = None + except Exception as e: + str_error = e + + if str_error: + sleep(sleep_time) # wait before trying to fetch the data again + sleep_time *= 2 # exponential backoff + else: + break + + if not str_error and response: + with open(self.TEMP_FILENAME, "wb") as handle: + for block in response.iter_content(1024): + handle.write(block) + + def _get_data_id(self, urn): + return urn.replace(":", "-") diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/local_data_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/local_data_service.py new file mode 100755 index 00000000..24d99597 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/local_data_service.py @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +import json + +from django.conf import settings + +from metax_api.tasks.refdata.refdata_indexer.domain.reference_data import ReferenceData + + +class LocalDataService: + """ + Service for getting reference data for elasticsearch index. The data is local, + i.e. data should exist on localhost. + """ + + LOCAL_REFDATA_FOLDER = settings.LOCAL_REF_DATA_FOLDER + + # move infra here beacuse the fetch API for it is broken and there is no estimate when it could be fixed. + # to keep the reference data unchanged, use the old scheme until some fix for it has been invented and + # validated. + INFRA_SCHEME = "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures" + + def get_data(self, data_type): + return self._parse_local_reference_data(data_type) + + def _parse_local_reference_data(self, data_type): + index_data_models = [] + with open(self.LOCAL_REFDATA_FOLDER + data_type + ".json", "r") as f: + data = json.load(f) + for item in data: + ref_item = ReferenceData( + item.get("id", ""), + data_type, + item.get("label", ""), + item.get("uri", ""), + same_as=item.get("same_as", []), + input_file_format=item.get("input_file_format", ""), + output_format_version=item.get("output_format_version", ""), + internal_code=item.get("internal_code", ""), + scheme=self.INFRA_SCHEME + if data_type == ReferenceData.DATA_TYPE_RESEARCH_INFRA + else "", + ) + + index_data_models.append(ref_item) + + return index_data_models diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/mime_data_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/mime_data_service.py new file mode 100755 index 00000000..9484292e --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/mime_data_service.py @@ -0,0 +1,132 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +import logging +import os +from time import sleep +from xml.etree import cElementTree as ET + +import requests + +from metax_api.tasks.refdata.refdata_indexer.domain.reference_data import ReferenceData +from metax_api.tasks.refdata.refdata_indexer.service.service_utils import file_exists + +_logger = logging.getLogger(__name__) + + +class MimeDataService: + """ + Service for getting mime type reference data for elasticsearch index. The data is in iana.org, + so it is first fetched and parsed. + """ + + IANA_NS = "{http://www.iana.org/assignments}" + MIME_TYPE_REF_DATA_SOURCE_URL = ( + "https://www.iana.org/assignments/media-types/media-types.xml" + ) + MIME_TYPE_REGISTRY_IDS = [ + "application", + "audio", + "font", + "image", + "message", + "model", + "multipart", + "text", + "video", + ] + + TEMP_XML_FILENAME = "/tmp/data.xml" + + def get_data(self): + self._fetch_mime_data() + + if not file_exists(self.TEMP_XML_FILENAME): + return [] + + index_data_models = self._parse_mime_data() + os.remove(self.TEMP_XML_FILENAME) + + return index_data_models + + def _parse_mime_data(self): + data_type = ReferenceData.DATA_TYPE_MIME_TYPE + index_data_models = [] + _logger.info("Extracting relevant data from the fetched data") + + is_parsing_model_elem = False + found_valid_file_elem = False + found_valid_name_elem = False + for event, elem in ET.iterparse( + self.TEMP_XML_FILENAME, events=("start", "end") + ): + if event == "start": + if ( + elem.tag == (self.IANA_NS + "registry") + and elem.get("id") in self.MIME_TYPE_REGISTRY_IDS + ): + is_parsing_model_elem = True + registry_name = elem.get("id") + if is_parsing_model_elem and elem.tag == (self.IANA_NS + "name"): + if elem.text: + found_valid_name_elem = True + uri = ( + "https://www.iana.org/assignments/media-types/" + + registry_name + + "/" + + elem.text + ) + data_id = registry_name + "/" + elem.text + if ( + is_parsing_model_elem + and elem.tag == (self.IANA_NS + "file") + and elem.get("type") == "template" + ): + if elem.text: + found_valid_file_elem = True + uri = ( + "https://www.iana.org/assignments/media-types/" + elem.text + ) + data_id = elem.text + elif event == "end": + if elem.tag == self.IANA_NS + "registry": + is_parsing_model_elem = False + if is_parsing_model_elem and elem.tag == (self.IANA_NS + "record"): + if found_valid_file_elem or found_valid_name_elem: + ref_item = ReferenceData( + data_id, + data_type, + {}, + uri, + scheme=self.MIME_TYPE_REF_DATA_SOURCE_URL, + ) + index_data_models.append(ref_item) + found_valid_file_elem = False + found_valid_name_elem = False + + return index_data_models + + def _fetch_mime_data(self): + url = self.MIME_TYPE_REF_DATA_SOURCE_URL + sleep_time = 2 + num_retries = 7 + + _logger.info("Fetching data from url " + url) + + for x in range(0, num_retries): + try: + response = requests.get(url, stream=True) + str_error = None + except Exception as e: + str_error = e + + if str_error: + sleep(sleep_time) # wait before trying to fetch the data again + sleep_time *= 2 # exponential backoff + else: + break + + if not str_error and response: + with open(self.TEMP_XML_FILENAME, "wb") as handle: + for block in response.iter_content(1024): + handle.write(block) diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/organization_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/organization_service.py new file mode 100755 index 00000000..9c863e73 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/organization_service.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +import json +import logging +import os + +import metax_api.tasks.refdata.refdata_indexer.organization_csv_parser as org_parser +from metax_api.tasks.refdata.refdata_indexer.domain.organization_data import ( + OrganizationData, +) + +logger = logging.getLogger(__name__) + + +class OrganizationService: + """ + Service for getting organization data for elasticsearch index + """ + + INPUT_FILE = org_parser.OUTPUT_FILE + + def get_data(self): + logger.info("parsing organizations") + # Parse csv files containing organizational data + org_parser.parse_csv() + + index_data_models = [] + with open(self.INPUT_FILE) as org_data_file: + data = json.load(org_data_file) + + for org in data: + parent_id = org.get("parent_id", "") + same_as = org.get("same_as", []) + org_csc = org.get("org_csc", "") + index_data_models.append( + OrganizationData( + org["org_id"], org["label"], parent_id, same_as, org_csc + ) + ) + + os.remove(self.INPUT_FILE) + return index_data_models diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/service_utils.py b/src/metax_api/tasks/refdata/refdata_indexer/service/service_utils.py new file mode 100755 index 00000000..44f9f681 --- /dev/null +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/service_utils.py @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: Copyright (c) 2018-2019 Ministry of Education and Culture, Finland +# +# SPDX-License-Identifier: GPL-3.0-or-later +from os.path import isfile + + +def set_default_label(label): + if label and len(label) > 0: + if "fi" in label: + label["und"] = label["fi"] + elif "en" in label: + label["und"] = label["en"] + else: + label["und"] = next(iter(label.values())) + + +def file_exists(file_path): + return isfile(file_path) diff --git a/src/metax_api/templates/secure/auth_success.html b/src/metax_api/templates/secure/auth_success.html old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/__init__.py b/src/metax_api/tests/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/__init__.py b/src/metax_api/tests/api/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/oaipmh/__init__.py b/src/metax_api/tests/api/oaipmh/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/oaipmh/minimal_api.py b/src/metax_api/tests/api/oaipmh/minimal_api.py old mode 100644 new mode 100755 index ef35776a..78e783d0 --- a/src/metax_api/tests/api/oaipmh/minimal_api.py +++ b/src/metax_api/tests/api/oaipmh/minimal_api.py @@ -5,9 +5,9 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import lxml.etree from django.conf import settings from django.core.management import call_command -import lxml.etree from lxml.etree import Element from oaipmh import common from rest_framework import status diff --git a/src/metax_api/tests/api/oaipmh/syke.py b/src/metax_api/tests/api/oaipmh/syke.py old mode 100644 new mode 100755 index 8648abe5..732038be --- a/src/metax_api/tests/api/oaipmh/syke.py +++ b/src/metax_api/tests/api/oaipmh/syke.py @@ -5,13 +5,13 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from django.core.management import call_command import lxml.etree +from django.core.management import call_command from rest_framework import status from rest_framework.test import APITestCase -from metax_api.models import CatalogRecord -from metax_api.api.oaipmh.base.metax_oai_server import SYKE_URL_PREFIX_TEMPLATE +from metax_api.api.oaipmh.base.metax_oai_server import SYKE_URL_PREFIX_TEMPLATE +from metax_api.models import CatalogRecord from metax_api.tests.utils import test_data_file_path, TestClassUtils diff --git a/src/metax_api/tests/api/rest/__init__.py b/src/metax_api/tests/api/rest/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/__init__.py b/src/metax_api/tests/api/rest/base/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/serializers/__init__.py b/src/metax_api/tests/api/rest/base/serializers/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/serializers/file_serializer.py b/src/metax_api/tests/api/rest/base/serializers/file_serializer.py old mode 100644 new mode 100755 index 87f2b751..4d2988be --- a/src/metax_api/tests/api/rest/base/serializers/file_serializer.py +++ b/src/metax_api/tests/api/rest/base/serializers/file_serializer.py @@ -10,8 +10,8 @@ from django.core.management import call_command from rest_framework.test import APITestCase -from metax_api.models import File from metax_api.api.rest.base.serializers import FileSerializer, LightFileSerializer +from metax_api.models import File from metax_api.tests.utils import test_data_file_path, TestClassUtils diff --git a/src/metax_api/tests/api/rest/base/serializers/serializer_utils.py b/src/metax_api/tests/api/rest/base/serializers/serializer_utils.py old mode 100644 new mode 100755 index b51422fc..c65f2550 --- a/src/metax_api/tests/api/rest/base/serializers/serializer_utils.py +++ b/src/metax_api/tests/api/rest/base/serializers/serializer_utils.py @@ -13,7 +13,6 @@ from metax_api.models import CatalogRecord from metax_api.tests.utils import test_data_file_path, get_json_schema, TestClassUtils - schema = get_json_schema('ida_dataset') diff --git a/src/metax_api/tests/api/rest/base/views/__init__.py b/src/metax_api/tests/api/rest/base/views/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/apierrors/__init__.py b/src/metax_api/tests/api/rest/base/views/apierrors/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/apierrors/read.py b/src/metax_api/tests/api/rest/base/views/apierrors/read.py old mode 100644 new mode 100755 index 19b4da24..6d01d42a --- a/src/metax_api/tests/api/rest/base/views/apierrors/read.py +++ b/src/metax_api/tests/api/rest/base/views/apierrors/read.py @@ -4,7 +4,7 @@ # # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT - +import logging from os import makedirs from shutil import rmtree @@ -13,7 +13,13 @@ from rest_framework import status from rest_framework.test import APITestCase -from metax_api.tests.utils import test_data_file_path, TestClassUtils +from metax_api.tests.utils import ( + test_data_file_path, + TestClassUtils, + testcase_log_console, +) + +_logger = logging.getLogger(__name__) class ApiErrorReadBasicTests(APITestCase, TestClassUtils): @@ -27,7 +33,7 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(ApiErrorReadBasicTests, cls).setUpClass() def setUp(self): @@ -35,94 +41,117 @@ def setUp(self): rmtree(settings.ERROR_FILES_PATH, ignore_errors=True) makedirs(settings.ERROR_FILES_PATH) metax_user = settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) + self._use_http_authorization( + username=metax_user["username"], password=metax_user["password"] + ) def _assert_fields_presence(self, response): """ Check presence and absence of some key information. """ self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('data' in response.data, True, response.data) - self.assertEqual('response' in response.data, True, response.data) - self.assertEqual('traceback' in response.data, True, response.data) - self.assertEqual('url' in response.data, True, response.data) - self.assertEqual('HTTP_AUTHORIZATION' in response.data['headers'], False, response.data['headers']) + self.assertEqual("data" in response.data, True, response.data) + self.assertEqual("response" in response.data, True, response.data) + self.assertEqual("traceback" in response.data, True, response.data) + self.assertEqual("url" in response.data, True, response.data) + self.assertEqual( + "HTTP_AUTHORIZATION" in response.data["headers"], + False, + response.data["headers"], + ) def test_list_errors(self): """ Each requesting resulting in an error should leave behind one API error entry. """ - cr_1 = self.client.get('/rest/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1.pop('data_catalog') # causes an error + cr_1 = self.client.get("/rest/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1.pop("data_catalog") # causes an error - response = self.client.post('/rest/datasets', cr_1, format='json') + response = self.client.post("/rest/datasets", cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.post('/rest/datasets', cr_1, format='json') + response = self.client.post("/rest/datasets", cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/apierrors') + response = self.client.get("/rest/apierrors") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), 2, response.data) def test_get_error_details(self): - cr_1 = self.client.get('/rest/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1.pop('data_catalog') # causes an error - cr_1['research_dataset']['title'] = { 'en': 'Abc' } + cr_1 = self.client.get("/rest/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1.pop("data_catalog") # causes an error + cr_1["research_dataset"]["title"] = {"en": "Abc"} - response = self.client.post('/rest/datasets', cr_1, format='json') + response = self.client.post("/rest/datasets", cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # list errors in order to get error identifier - response = self.client.get('/rest/apierrors') + response = self.client.get("/rest/apierrors") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('identifier' in response.data[0], True, response.data) + self.assertEqual("identifier" in response.data[0], True, response.data) - response = self.client.get('/rest/apierrors/%s' % response.data[0]['identifier']) + response = self.client.get( + "/rest/apierrors/%s" % response.data[0]["identifier"] + ) self._assert_fields_presence(response) - self.assertEqual('data_catalog' in response.data['response'], True, response.data['response']) - self.assertEqual(response.data['data']['research_dataset']['title']['en'], 'Abc', - response.data['data']['research_dataset']['title']) - + self.assertEqual( + "data_catalog" in response.data["response"], True, response.data["response"] + ) + self.assertEqual( + response.data["data"]["research_dataset"]["title"]["en"], + "Abc", + response.data["data"]["research_dataset"]["title"], + ) + + @testcase_log_console(_logger) def test_delete_error_details(self): - cr_1 = self.client.get('/rest/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1.pop('data_catalog') # causes an error + cr_1 = self.client.get("/rest/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1.pop("data_catalog") # causes an error - response = self.client.post('/rest/datasets', cr_1, format='json') + response = self.client.post("/rest/datasets", cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/apierrors') - response = self.client.delete('/rest/apierrors/%s' % response.data[0]['identifier']) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) + response = self.client.get("/rest/apierrors") + response = self.client.delete( + "/rest/apierrors/%s" % response.data[0]["identifier"] + ) + self.assertEqual( + response.status_code, status.HTTP_204_NO_CONTENT, response.data + ) - response = self.client.get('/rest/apierrors') + response = self.client.get("/rest/apierrors") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), 0, response.data) + @testcase_log_console(_logger) def test_delete_all_error_details(self): - cr_1 = self.client.get('/rest/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1.pop('data_catalog') # causes an error - - response = self.client.post('/rest/datasets', cr_1, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - response = self.client.post('/rest/datasets', cr_1, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + cr_1 = self.client.get("/rest/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1.pop("data_catalog") # causes an error + + response = self.client.post("/rest/datasets", cr_1, format="json") + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) + response = self.client.post("/rest/datasets", cr_1, format="json") + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, response.data + ) # ensure something was produced... - response = self.client.get('/rest/apierrors') + response = self.client.get("/rest/apierrors") self.assertEqual(len(response.data), 2, response.data) - response = self.client.post('/rest/apierrors/flush') + response = self.client.post("/rest/apierrors/flush") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/apierrors') + response = self.client.get("/rest/apierrors") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), 0, response.data) @@ -130,31 +159,35 @@ def test_bulk_operation_produces_error_entry(self): """ Ensure also bulk operations produce error entries. """ - cr_1 = self.client.get('/rest/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1.pop('data_catalog') # causes an error - response = self.client.post('/rest/datasets', [cr_1, cr_1], format='json') + cr_1 = self.client.get("/rest/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1.pop("data_catalog") # causes an error + response = self.client.post("/rest/datasets", [cr_1, cr_1], format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/apierrors') + response = self.client.get("/rest/apierrors") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), 1, response.data) - response = self.client.get('/rest/apierrors/%s' % response.data[0]['identifier']) + response = self.client.get( + "/rest/apierrors/%s" % response.data[0]["identifier"] + ) self._assert_fields_presence(response) - self.assertEqual('other' in response.data, True, response.data) - self.assertEqual('bulk_request' in response.data['other'], True, response.data) - self.assertEqual('data_row_count' in response.data['other'], True, response.data) + self.assertEqual("other" in response.data, True, response.data) + self.assertEqual("bulk_request" in response.data["other"], True, response.data) + self.assertEqual( + "data_row_count" in response.data["other"], True, response.data + ) def test_api_permitted_only_to_metax_user(self): # uses testuser by default self._use_http_authorization() - response = self.client.get('/rest/apierrors') + response = self.client.get("/rest/apierrors") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.get('/rest/apierrors/123') + response = self.client.get("/rest/apierrors/123") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.delete('/rest/apierrors/123') + response = self.client.delete("/rest/apierrors/123") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.post('/rest/apierrors/flush_errors') + response = self.client.post("/rest/apierrors/flush_errors") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/src/metax_api/tests/api/rest/base/views/common/__init__.py b/src/metax_api/tests/api/rest/base/views/common/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/common/auth.py b/src/metax_api/tests/api/rest/base/views/common/auth.py old mode 100644 new mode 100755 index 6d2eba96..744f9216 --- a/src/metax_api/tests/api/rest/base/views/common/auth.py +++ b/src/metax_api/tests/api/rest/base/views/common/auth.py @@ -9,16 +9,18 @@ import logging import os -from django.conf import settings import responses +from django.conf import settings from rest_framework import status -from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon +from metax_api.tests.api.rest.base.views.datasets.write import ( + CatalogRecordApiWriteCommon, +) from metax_api.tests.utils import get_test_oidc_token - _logger = logging.getLogger(__name__) + class ApiServiceAccessAuthorization(CatalogRecordApiWriteCommon): """ @@ -28,13 +30,13 @@ class ApiServiceAccessAuthorization(CatalogRecordApiWriteCommon): def setUp(self): super().setUp() # test user api_auth_user has some custom api permissions set in settings.py - self._use_http_authorization(username='api_auth_user') + self._use_http_authorization(username="api_auth_user") def test_read_access_ok(self): """ User api_auth_user should have read access to files api. """ - response = self.client.get('/rest/files/1') + response = self.client.get("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_200_OK) def test_read_access_fail(self): @@ -43,36 +45,36 @@ def test_read_access_fail(self): about the existence of requested file. """ self.client._credentials = {} - response = self.client.get('/rest/files/1') + response = self.client.get("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_create_access_ok(self): """ User api_auth_user should have create access to datasets api. """ - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") cr = response.data - cr['contract'] = 1 - response = self.client.put('/rest/datasets/1', cr, format='json') + cr["contract"] = 1 + response = self.client.put("/rest/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) def test_update_access_error(self): """ User api_auth_user should not have update access to files api. """ - response = self.client.get('/rest/files/1') + response = self.client.get("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_200_OK) file = response.data - file['file_format'] = 'text/html' + file["file_format"] = "text/html" - response = self.client.put('/rest/files/1', file, format='json') + response = self.client.put("/rest/files/1", file, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_delete_access_error(self): """ User api_auth_user should not have delete access to files api. """ - response = self.client.delete('/rest/files/1') + response = self.client.delete("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_read_for_datasets_world_ok(self): @@ -81,7 +83,7 @@ def test_read_for_datasets_world_ok(self): """ self.client._credentials = {} - for req in ['/rest/datasets', '/rest/datasets/1']: + for req in ["/rest/datasets", "/rest/datasets/1"]: response = self.client.get(req) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -90,7 +92,7 @@ def test_read_for_datacatalogs_world_ok(self): Reading datacatalogs api should be permitted even without any authorization. """ self.client._credentials = {} - for req in ['/rest/datacatalogs', '/rest/datacatalogs/1']: + for req in ["/rest/datacatalogs", "/rest/datacatalogs/1"]: response = self.client.get(req) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -107,7 +109,7 @@ class ApiEndUserAccessAuthorization(CatalogRecordApiWriteCommon): def setUp(self): super().setUp() - self._use_http_authorization(method='bearer', token=get_test_oidc_token()) + self._use_http_authorization(method="bearer", token=get_test_oidc_token()) @responses.activate def test_valid_token(self): @@ -116,7 +118,7 @@ def test_valid_token(self): valid authentication works. Should return successfully. """ self._mock_token_validation_succeeds() - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK) @responses.activate @@ -138,7 +140,7 @@ def test_invalid_token(self): In all cases, metax code execution stops at the middleware where authentication failed. """ self._mock_token_validation_fails() - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @responses.activate @@ -149,11 +151,11 @@ def test_end_user_read_access(self): self._mock_token_validation_succeeds() # datasets-api should be allowed for end users - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK) # contracts-api should not be allowed for end users - response = self.client.get('/rest/contracts/1') + response = self.client.get("/rest/contracts/1") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @responses.activate @@ -163,13 +165,14 @@ def test_end_user_create_access_error(self): """ self._mock_token_validation_succeeds() # end users should not have create access to files api. - response = self.client.post('/rest/files', {}, format='json') + response = self.client.post("/rest/files", {}, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) def test_removing_bearer_from_allowed_auth_methods_disables_oidc(self): pass # ALLOWED_AUTH_METHODS + class ApiEndUserAdditionalProjects(CatalogRecordApiWriteCommon): """ @@ -178,7 +181,9 @@ class ApiEndUserAdditionalProjects(CatalogRecordApiWriteCommon): def setUp(self): super().setUp() - self._use_http_authorization(method='bearer', token=get_test_oidc_token(new_proxy=True)) + self._use_http_authorization( + method="bearer", token=get_test_oidc_token(new_proxy=True) + ) self._mock_token_validation_succeeds() def tearDown(self): @@ -186,40 +191,33 @@ def tearDown(self): try: os.remove(settings.ADDITIONAL_USER_PROJECTS_PATH) except: - _logger.info("error removing file from %s" % settings.ADDITIONAL_USER_PROJECTS_PATH) + _logger.info( + "error removing file from %s" % settings.ADDITIONAL_USER_PROJECTS_PATH + ) @responses.activate def test_successful_read(self): """ Ensures user's file projects are also fetched from local file. """ - testdata = { "testuser": ["some_project", "project_x"] } - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'w+') as testfile: + testdata = {"testuser": ["some_project", "project_x"]} + with open(settings.ADDITIONAL_USER_PROJECTS_PATH, "w+") as testfile: json.dump(testdata, testfile, indent=4) os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - response = self.client.get('/rest/files?project_identifier=project_x', format='json') + response = self.client.get( + "/rest/files?project_identifier=project_x", format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - @responses.activate - def test_no_file_permission(self): - """ - Ensures user's file projects are also fetched from local file. - """ - testdata = { "testuser": ["project_x"] } - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'w+') as testfile: - json.dump(testdata, testfile, indent=4) - os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o100) - - response = self.client.get('/rest/files?project_identifier=project_x', format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - @responses.activate def test_no_file(self): """ Projects are fetched from token when local file is not available. """ - response = self.client.get('/rest/files?project_identifier=2001036', format='json') + response = self.client.get( + "/rest/files?project_identifier=2001036", format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) @responses.activate @@ -227,12 +225,14 @@ def test_bad_file_keys(self): """ Must return forbidden which indicates that code is run properly despite the bad local file. """ - testdata = { 123445: "project_x" } - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'w+') as testfile: + testdata = {123445: "project_x"} + with open(settings.ADDITIONAL_USER_PROJECTS_PATH, "w+") as testfile: json.dump(testdata, testfile, indent=4) os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - response = self.client.get('/rest/files?project_identifier=project_x', format='json') + response = self.client.get( + "/rest/files?project_identifier=project_x", format="json" + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) @responses.activate @@ -240,12 +240,14 @@ def test_bad_file_values(self): """ Returns forbidden since values on local file are not list of strings. """ - testdata = { "testuser": "project_x" } - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'w+') as testfile: + testdata = {"testuser": "project_x"} + with open(settings.ADDITIONAL_USER_PROJECTS_PATH, "w+") as testfile: json.dump(testdata, testfile, indent=4) os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - response = self.client.get('/rest/files?project_identifier=project_x', format='json') + response = self.client.get( + "/rest/files?project_identifier=project_x", format="json" + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) @responses.activate @@ -253,10 +255,12 @@ def test_bad_file_successful(self): """ Ensures that projects are read from token despite file reading is failed. """ - testdata = { "testuser": [151342, 236314] } - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'w+') as testfile: + testdata = {"testuser": [151342, 236314]} + with open(settings.ADDITIONAL_USER_PROJECTS_PATH, "w+") as testfile: json.dump(testdata, testfile, indent=4) os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - response = self.client.get('/rest/files?project_identifier=2001036', format='json') + response = self.client.get( + "/rest/files?project_identifier=2001036", format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) diff --git a/src/metax_api/tests/api/rest/base/views/common/read.py b/src/metax_api/tests/api/rest/base/views/common/read.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/common/write.py b/src/metax_api/tests/api/rest/base/views/common/write.py old mode 100644 new mode 100755 index 3c918702..3dfeb1df --- a/src/metax_api/tests/api/rest/base/views/common/write.py +++ b/src/metax_api/tests/api/rest/base/views/common/write.py @@ -16,7 +16,6 @@ from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon from metax_api.tests.utils import test_data_file_path, TestClassUtils - """ Common phenomenas that concern all API's. diff --git a/src/metax_api/tests/api/rest/base/views/contracts/__init__.py b/src/metax_api/tests/api/rest/base/views/contracts/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/contracts/contracts.py b/src/metax_api/tests/api/rest/base/views/contracts/contracts.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/datacatalogs/__init__.py b/src/metax_api/tests/api/rest/base/views/datacatalogs/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/datacatalogs/read.py b/src/metax_api/tests/api/rest/base/views/datacatalogs/read.py old mode 100644 new mode 100755 index 74c35576..776d550a --- a/src/metax_api/tests/api/rest/base/views/datacatalogs/read.py +++ b/src/metax_api/tests/api/rest/base/views/datacatalogs/read.py @@ -39,4 +39,4 @@ def test_allowed_read_methods(self): response = self.client.head(req) self.assertEqual(response.status_code, status.HTTP_200_OK) response = self.client.options(req) - self.assertEqual(response.status_code, status.HTTP_200_OK) \ No newline at end of file + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/metax_api/tests/api/rest/base/views/datacatalogs/write.py b/src/metax_api/tests/api/rest/base/views/datacatalogs/write.py old mode 100644 new mode 100755 index fefdfea5..3932bf34 --- a/src/metax_api/tests/api/rest/base/views/datacatalogs/write.py +++ b/src/metax_api/tests/api/rest/base/views/datacatalogs/write.py @@ -11,7 +11,7 @@ from rest_framework.test import APITestCase from metax_api.models import DataCatalog -from metax_api.services import RedisCacheService as cache +from metax_api.services.redis_cache_service import RedisClient from metax_api.tests.utils import test_data_file_path, TestClassUtils @@ -110,6 +110,7 @@ def test_create_data_catalog_populate_fields_from_reference_data(self): codes to uris after a successful create 3) Check that labels have also been copied to data catalog to their approriate fields """ + cache = RedisClient() refdata = cache.get('reference_data')['reference_data'] orgdata = cache.get('reference_data')['organization_data'] refs = {} diff --git a/src/metax_api/tests/api/rest/base/views/datasets/__init__.py b/src/metax_api/tests/api/rest/base/views/datasets/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/datasets/read.py b/src/metax_api/tests/api/rest/base/views/datasets/read.py old mode 100644 new mode 100755 index e4c926c3..3fb9ae0e --- a/src/metax_api/tests/api/rest/base/views/datasets/read.py +++ b/src/metax_api/tests/api/rest/base/views/datasets/read.py @@ -5,14 +5,14 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from datetime import timedelta import urllib.parse +from datetime import timedelta +import responses from django.conf import settings from django.core.management import call_command from django.utils import timezone from pytz import timezone as tz -import responses from rest_framework import status from rest_framework.test import APITestCase diff --git a/src/metax_api/tests/api/rest/base/views/datasets/write.py b/src/metax_api/tests/api/rest/base/views/datasets/write.py old mode 100644 new mode 100755 index 5ce2c187..a1544384 --- a/src/metax_api/tests/api/rest/base/views/datasets/write.py +++ b/src/metax_api/tests/api/rest/base/views/datasets/write.py @@ -5,10 +5,10 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import unittest from copy import deepcopy from datetime import datetime, timedelta from time import sleep -import unittest import responses from django.conf import settings as django_settings @@ -17,10 +17,11 @@ from rest_framework.test import APITestCase from metax_api.models import AlternateRecordSet, CatalogRecord, Contract, DataCatalog, Directory, File -from metax_api.services import ReferenceDataMixin as RDM, RedisCacheService as cache +from metax_api.models.catalog_record import ACCESS_TYPES +from metax_api.services import ReferenceDataMixin as RDM +from metax_api.services.redis_cache_service import RedisClient from metax_api.tests.utils import get_test_oidc_token, test_data_file_path, TestClassUtils from metax_api.utils import get_tz_aware_now_without_micros, get_identifier_type, IdentifierType -from metax_api.models.catalog_record import ACCESS_TYPES VALIDATE_TOKEN_URL = django_settings.VALIDATE_TOKEN_URL END_USER_ALLOWED_DATA_CATALOGS = django_settings.END_USER_ALLOWED_DATA_CATALOGS @@ -846,6 +847,19 @@ def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_3( self.assertEqual('preferred_identifier' in response.data['research_dataset'][0], True, 'The error should be about preferred_identifier already existing') + def test_remote_doi_dataset_is_validated_against_datacite_format(self): + # Remote input DOI ids need to take datasets for datacite validation + cr = {'research_dataset': self.cr_test_data['research_dataset']} + cr['research_dataset']['preferred_identifier'] = 'doi:10.5061/dryad.10188854' + cr['data_catalog'] = 3 + cr['metadata_provider_org'] = 'metax' + cr['metadata_provider_user'] = 'metax' + cr['research_dataset'].pop('publisher', None) + + response = self.client.post('/rest/datasets', cr, format="json") + # Publisher value is required for datacite format, so this should return Http400 + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual('a required value for datacite format' in response.data['detail'][0], True, response.data) # # helpers # @@ -863,7 +877,6 @@ def _set_preferred_identifier_to_record(self, pk=None, catalog_id=None): cr._handle_preferred_identifier_changed() return unique_identifier - class CatalogRecordApiWriteDatasetSchemaSelection(CatalogRecordApiWriteCommon): # # @@ -1125,7 +1138,7 @@ def test_catalog_record_deprecation_updates_date_modified(self): cr_depr = CatalogRecord.objects.get(identifier=cr_id) self.assertTrue(cr_depr.deprecated) - self.assertEqual(cr_depr.date_modified, cr_depr.date_deprecated, 'date_modified should be updated') + # self.assertEqual(cr_depr.date_modified, cr_depr.date_deprecated, 'date_modified should be updated') def test_change_datacatalog_ATT_to_IDA(self): cr = self._get_new_full_test_att_cr_data() @@ -1301,6 +1314,81 @@ def test_delete_catalog_record_error_using_preferred_identifier(self): response = self.client.delete(url) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_bulk_delete_catalog_record_permissions(self): + # create catalog with 'metax' edit permissions and create dataset with this catalog as 'metax' user + cr = self._get_new_test_cr_data() + cr.pop('id') + catalog = self._get_object_from_test_data('datacatalog', requested_index=0) + catalog.pop('id') + catalog['catalog_json']['identifier'] = 'metax-catalog' + catalog['catalog_record_services_edit'] = 'metax' + catalog = self.client.post('/rest/datacatalogs', catalog, format="json") + cr['data_catalog'] = {'id': catalog.data['id'], 'identifier': catalog.data['catalog_json']['identifier']} + + self._use_http_authorization(username='metax') + response = self.client.post('/rest/datasets/', cr, format="json") + metax_cr = response.data['id'] + + # create catalog with 'testuser' edit permissions and create dataset with this catalog as 'testuser' user + cr = self._get_new_test_cr_data() + cr.pop('id') + catalog = self._get_object_from_test_data('datacatalog', requested_index=1) + catalog.pop('id') + catalog['catalog_json']['identifier'] = 'testuser-catalog' + catalog['catalog_record_services_edit'] = 'testuser' + catalog = self.client.post('/rest/datacatalogs', catalog, format="json") + cr['data_catalog'] = {'id': catalog.data['id'], 'identifier': catalog.data['catalog_json']['identifier']} + + self._use_http_authorization(username='testuser', password='testuserpassword') + response = self.client.post('/rest/datasets/', cr, format="json") + testuser_cr = response.data['id'] + + # after trying to delete as 'testuser' only one catalog is deleted + response = self.client.delete('/rest/datasets', [metax_cr, testuser_cr], format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, [testuser_cr]) + response = self.client.post('/rest/datasets/list?pagination=false', [metax_cr, testuser_cr], format="json") + self.assertTrue(len(response.data), 1) + + response = self.client.delete('/rest/datasets', [metax_cr], format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + response = self.client.post('/rest/datasets/list?pagination=false', [metax_cr, testuser_cr], format="json") + self.assertTrue(len(response.data), 1) + + def test_bulk_delete_catalog_record(self): + ids = [1, 2, 3] + identifiers = CatalogRecord.objects.filter(pk__in=[4, 5, 6]).values_list('identifier', flat=True) + + for crs in [ids, identifiers]: + response = self.client.delete('/rest/datasets', crs, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.data == [1, 2, 3] or response.data == [4, 5, 6]) + response = self.client.post('/rest/datasets/list?pagination=false', crs, format="json") + self.assertFalse(response.data) + + for cr in crs: + if isinstance(cr, int): + deleted = CatalogRecord.objects_unfiltered.get(id=cr) + else: + deleted = CatalogRecord.objects_unfiltered.get(identifier=cr) + + self.assertEqual(deleted.removed, True) + self.assertEqual(deleted.date_modified, deleted.date_removed, + 'date_modified should be updated') + + # failing tests + ids = [1000, 2000] + identifiers = ['1000', '2000'] + + for crs in [ids, identifiers]: + response = self.client.delete('/rest/datasets', ids, format="json") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + ids = [] + response = self.client.delete('/rest/datasets', ids, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertTrue('Received empty list of identifiers' in response.data['detail'][0]) + class CatalogRecordApiWritePreservationStateTests(CatalogRecordApiWriteCommon): @@ -1605,6 +1693,7 @@ def test_catalog_record_reference_data_missing_ok(self): The API should attempt to reload the reference data if it is missing from cache for whatever reason, and successfully finish the request """ + cache = RedisClient() cache.delete('reference_data') self.assertEqual(cache.get('reference_data', master=True), None, 'cache ref data should be missing after cache.delete()') @@ -1686,6 +1775,7 @@ def test_create_catalog_record_populate_fields_from_reference_data(self): codes to uris after a successful create 3) Check that labels have also been copied to datasets to their approriate fields """ + cache = RedisClient() rf = RDM.get_reference_data(cache) refdata = rf['reference_data'] orgdata = rf['organization_data'] @@ -4116,7 +4206,7 @@ def test_harvested_catalogs_must_have_preferred_identifier_create(self): @unittest.skipIf(django_settings.REMS['ENABLED'] is not True, 'Only run if REMS is enabled') class CatalogRecordApiWriteREMS(CatalogRecordApiWriteCommon): - + cache = RedisClient() rf = RDM.get_reference_data(cache) # get by code to prevent failures if list ordering changes access_permit = [type for type in rf['reference_data']['access_type'] if type['code'] == 'permit'][0] diff --git a/src/metax_api/tests/api/rest/base/views/directories/__init__.py b/src/metax_api/tests/api/rest/base/views/directories/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/directories/read.py b/src/metax_api/tests/api/rest/base/views/directories/read.py old mode 100644 new mode 100755 index f265f454..1f03f92d --- a/src/metax_api/tests/api/rest/base/views/directories/read.py +++ b/src/metax_api/tests/api/rest/base/views/directories/read.py @@ -5,11 +5,11 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT import copy -import responses +import responses +from django.core.management import call_command from django.db import transaction from django.db.models import Count, Sum -from django.core.management import call_command from rest_framework import status from rest_framework.test import APITestCase diff --git a/src/metax_api/tests/api/rest/base/views/directories/write.py b/src/metax_api/tests/api/rest/base/views/directories/write.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/files/__init__.py b/src/metax_api/tests/api/rest/base/views/files/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/files/read.py b/src/metax_api/tests/api/rest/base/views/files/read.py old mode 100644 new mode 100755 index ec620663..4630a93d --- a/src/metax_api/tests/api/rest/base/views/files/read.py +++ b/src/metax_api/tests/api/rest/base/views/files/read.py @@ -5,11 +5,11 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import responses from django.core.management import call_command from django.db import connection from rest_framework import status from rest_framework.test import APITestCase -import responses from metax_api.models import File from metax_api.tests.utils import get_test_oidc_token, test_data_file_path, TestClassUtils diff --git a/src/metax_api/tests/api/rest/base/views/files/write.py b/src/metax_api/tests/api/rest/base/views/files/write.py old mode 100644 new mode 100755 index 13f2bee5..bf10d202 --- a/src/metax_api/tests/api/rest/base/views/files/write.py +++ b/src/metax_api/tests/api/rest/base/views/files/write.py @@ -8,14 +8,14 @@ from copy import deepcopy from os.path import dirname -from django.db.models import Sum +import responses from django.core.management import call_command +from django.db.models import Sum from rest_framework import status from rest_framework.test import APITestCase -import responses from metax_api.models import CatalogRecord, Directory, File -from metax_api.services import RedisCacheService as cache +from metax_api.services.redis_cache_service import RedisClient from metax_api.tests.utils import get_test_oidc_token, test_data_file_path, TestClassUtils @@ -109,6 +109,7 @@ def setUpClass(cls): def setUp(self): super().setUp() + cache = RedisClient() ffv_refdata = cache.get('reference_data')['reference_data']['file_format_version'] # File format version entry in reference data that has some output_format_version diff --git a/src/metax_api/tests/api/rest/base/views/filestorages/__init__.py b/src/metax_api/tests/api/rest/base/views/filestorages/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/filestorages/read.py b/src/metax_api/tests/api/rest/base/views/filestorages/read.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/filestorages/write.py b/src/metax_api/tests/api/rest/base/views/filestorages/write.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/schemas/__init__.py b/src/metax_api/tests/api/rest/base/views/schemas/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/base/views/schemas/read.py b/src/metax_api/tests/api/rest/base/views/schemas/read.py old mode 100644 new mode 100755 index 942afbb0..eea13fd6 --- a/src/metax_api/tests/api/rest/base/views/schemas/read.py +++ b/src/metax_api/tests/api/rest/base/views/schemas/read.py @@ -5,9 +5,11 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +from rest_framework import status from rest_framework.test import APITestCase + from metax_api.tests.utils import TestClassUtils -from rest_framework import status + class SchemaApiReadTests(APITestCase, TestClassUtils): diff --git a/src/metax_api/tests/api/rest/v2/__init__.py b/src/metax_api/tests/api/rest/v2/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/__init__.py b/src/metax_api/tests/api/rest/v2/views/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/apierrors/__init__.py b/src/metax_api/tests/api/rest/v2/views/apierrors/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/apierrors/read.py b/src/metax_api/tests/api/rest/v2/views/apierrors/read.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/common/__init__.py b/src/metax_api/tests/api/rest/v2/views/common/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/common/auth.py b/src/metax_api/tests/api/rest/v2/views/common/auth.py old mode 100644 new mode 100755 index 2eb91726..ea5c9730 --- a/src/metax_api/tests/api/rest/v2/views/common/auth.py +++ b/src/metax_api/tests/api/rest/v2/views/common/auth.py @@ -5,18 +5,14 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -import json import logging -import os -from django.conf import settings import responses from rest_framework import status from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon from metax_api.tests.utils import get_test_oidc_token - _logger = logging.getLogger(__name__) class ApiServiceAccessAuthorization(CatalogRecordApiWriteCommon): @@ -66,7 +62,7 @@ def test_update_access_error(self): file['file_format'] = 'text/html' response = self.client.put('/rest/v2/files/1', file, format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) def test_delete_access_error(self): """ @@ -154,98 +150,3 @@ def test_end_user_create_access_error(self): # end users should not have create access to files api. response = self.client.post('/rest/v2/files', {}, format='json') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_removing_bearer_from_allowed_auth_methods_disables_oidc(self): - pass - # ALLOWED_AUTH_METHODS - -class ApiEndUserAdditionalProjects(CatalogRecordApiWriteCommon): - - """ - Test reading additional permissions from local file - """ - - def setUp(self): - super().setUp() - self._use_http_authorization(method='bearer', token=get_test_oidc_token(new_proxy=True)) - self._mock_token_validation_succeeds() - - def tearDown(self): - super().tearDown() - try: - os.remove(settings.ADDITIONAL_USER_PROJECTS_PATH) - except: - _logger.info("error removing file from %s" % settings.ADDITIONAL_USER_PROJECTS_PATH) - - @responses.activate - def test_successful_read(self): - """ - Ensures user's file projects are also fetched from local file. - """ - testdata = { "testuser": ["some_project", "project_x"] } - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'w+') as testfile: - json.dump(testdata, testfile, indent=4) - os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - - response = self.client.get('/rest/v2/files?project_identifier=project_x', format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - @responses.activate - def test_no_file_permission(self): - """ - Ensures user's file projects are also fetched from local file. - """ - testdata = { "testuser": ["project_x"] } - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'w+') as testfile: - json.dump(testdata, testfile, indent=4) - os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o100) - - response = self.client.get('/rest/v2/files?project_identifier=project_x', format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - @responses.activate - def test_no_file(self): - """ - Projects are fetched from token when local file is not available. - """ - response = self.client.get('/rest/v2/files?project_identifier=2001036', format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - @responses.activate - def test_bad_file_keys(self): - """ - Must return forbidden which indicates that code is run properly despite the bad local file. - """ - testdata = { 123445: "project_x" } - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'w+') as testfile: - json.dump(testdata, testfile, indent=4) - os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - - response = self.client.get('/rest/v2/files?project_identifier=project_x', format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - @responses.activate - def test_bad_file_values(self): - """ - Returns forbidden since values on local file are not list of strings. - """ - testdata = { "testuser": "project_x" } - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'w+') as testfile: - json.dump(testdata, testfile, indent=4) - os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - - response = self.client.get('/rest/v2/files?project_identifier=project_x', format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - @responses.activate - def test_bad_file_successful(self): - """ - Ensures that projects are read from token despite file reading is failed. - """ - testdata = { "testuser": [151342, 236314] } - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'w+') as testfile: - json.dump(testdata, testfile, indent=4) - os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - - response = self.client.get('/rest/v2/files?project_identifier=2001036', format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) diff --git a/src/metax_api/tests/api/rest/v2/views/common/read.py b/src/metax_api/tests/api/rest/v2/views/common/read.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/common/write.py b/src/metax_api/tests/api/rest/v2/views/common/write.py old mode 100644 new mode 100755 index c5c576c7..8d6a5a88 --- a/src/metax_api/tests/api/rest/v2/views/common/write.py +++ b/src/metax_api/tests/api/rest/v2/views/common/write.py @@ -16,7 +16,6 @@ from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon from metax_api.tests.utils import test_data_file_path, TestClassUtils - """ Common phenomenas that concern all API's. diff --git a/src/metax_api/tests/api/rest/v2/views/contracts/__init__.py b/src/metax_api/tests/api/rest/v2/views/contracts/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/contracts/contracts.py b/src/metax_api/tests/api/rest/v2/views/contracts/contracts.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/datacatalogs/__init__.py b/src/metax_api/tests/api/rest/v2/views/datacatalogs/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/datacatalogs/read.py b/src/metax_api/tests/api/rest/v2/views/datacatalogs/read.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/datacatalogs/write.py b/src/metax_api/tests/api/rest/v2/views/datacatalogs/write.py old mode 100644 new mode 100755 index 7ce37a88..0c40ac88 --- a/src/metax_api/tests/api/rest/v2/views/datacatalogs/write.py +++ b/src/metax_api/tests/api/rest/v2/views/datacatalogs/write.py @@ -10,7 +10,7 @@ from rest_framework.test import APITestCase from metax_api.models import DataCatalog -from metax_api.services import RedisCacheService as cache +from metax_api.services.redis_cache_service import RedisClient from metax_api.tests.utils import test_data_file_path, TestClassUtils @@ -96,6 +96,7 @@ def test_create_data_catalog_populate_fields_from_reference_data(self): codes to uris after a successful create 3) Check that labels have also been copied to data catalog to their approriate fields """ + cache = RedisClient() refdata = cache.get('reference_data')['reference_data'] orgdata = cache.get('reference_data')['organization_data'] refs = {} diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/__init__.py b/src/metax_api/tests/api/rest/v2/views/datasets/__init__.py old mode 100644 new mode 100755 index 2237a643..548b55da --- a/src/metax_api/tests/api/rest/v2/views/datasets/__init__.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/__init__.py @@ -5,12 +5,11 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from .read import * -from .write import * - +from .api_version_lock import * from .drafts import * from .filehandling import * from .pas import * +from .read import * from .referencedata import * from .rems import * -from .api_version_lock import * \ No newline at end of file +from .write import * diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/api_version_lock.py b/src/metax_api/tests/api/rest/v2/views/datasets/api_version_lock.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py b/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py old mode 100644 new mode 100755 index 0fa97d3c..a0e79852 --- a/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py @@ -13,15 +13,15 @@ CatalogRecordV2, DataCatalog, ) -from metax_api.utils import get_tz_aware_now_without_micros from metax_api.tests.utils import get_test_oidc_token +from metax_api.utils import get_tz_aware_now_without_micros from .write import CatalogRecordApiWriteCommon - CR = CatalogRecordV2 END_USER_ALLOWED_DATA_CATALOGS = django_settings.END_USER_ALLOWED_DATA_CATALOGS IDA_CATALOG = django_settings.IDA_DATA_CATALOG_IDENTIFIER DFT_CATALOG = django_settings.DFT_DATA_CATALOG_IDENTIFIER +ATT_CATALOG = django_settings.ATT_DATA_CATALOG_IDENTIFIER class CatalogRecordDraftTests(CatalogRecordApiWriteCommon): """ @@ -41,7 +41,8 @@ def setUp(self): catalog_json=catalog_json, date_created=get_tz_aware_now_without_micros(), catalog_record_services_create='testuser,api_auth_user,metax', - catalog_record_services_edit='testuser,api_auth_user,metax' + catalog_record_services_edit='testuser,api_auth_user,metax', + catalog_record_services_read='testuser,api_auth_user,metax' ) self.minimal_draft = { @@ -381,8 +382,9 @@ def test_minimal_draft_dataset_creation(self): response = self.client.post('/rest/v2/datasets?draft', self.minimal_draft, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - def test_no_files_or_dirs_in_draft_catalog(self): - ''' Files cannot be added to datasets that are in draft catalog ''' + def test_allow_files_and_dirs_in_draft_catalog(self): + ''' Files can be added to datasets that are in draft catalog ''' + self._use_http_authorization(method='basic', username='metax') for type in ['files', 'directories']: self.minimal_draft['research_dataset'][type] = [ @@ -392,8 +394,11 @@ def test_no_files_or_dirs_in_draft_catalog(self): ] response = self.client.post('/rest/v2/datasets?draft', self.minimal_draft, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('files in draft catalog' in response.data['detail'][0], response.data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + response = self.client.get(f'/rest/v2/datasets/{response.data["id"]}/files', format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertTrue(response.data, response.data) self.minimal_draft['research_dataset'].pop(type) @@ -422,6 +427,36 @@ def test_prevent_update_published_dataset_to_draft_catalog(self): response = self.client.put(f'/rest/v2/datasets/{cr["id"]}', cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + def test_allow_remote_resources_in_ida_for_drafts(self): + """ + When dataset is in draft state, it should be validated with dft catalog + """ + self.cr_test_data['data_catalog'] = {"identifier": IDA_CATALOG} + self.cr_test_data['research_dataset']['remote_resources'] = [ + { + "title": "some title", + "use_category": {"identifier": "source"} + } + ] + + response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + def test_allow_file_additions_to_drafts(self): + """ + Files can be added later and the metadata can be modified via RPC apis. + """ + files = {"files": [ {"identifier": "pid:urn:5"} ]} + for catalog in [IDA_CATALOG, ATT_CATALOG, DFT_CATALOG]: + self.cr_test_data['data_catalog'] = {"identifier": catalog} + response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + cr = response.data + response = self.client.post(f'/rest/v2/datasets/{cr["id"]}/files', files, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertEqual(response.data['files_added'], 1, response.data) + class CatalogRecordDraftsOfPublished(CatalogRecordApiWriteCommon): """ diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/filehandling.py b/src/metax_api/tests/api/rest/v2/views/datasets/filehandling.py old mode 100644 new mode 100755 index a024daca..f08db04d --- a/src/metax_api/tests/api/rest/v2/views/datasets/filehandling.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/filehandling.py @@ -18,7 +18,6 @@ from metax_api.tests.utils import get_test_oidc_token from .write import CatalogRecordApiWriteCommon, create_end_user_catalogs - CR = CatalogRecordV2 diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/pas.py b/src/metax_api/tests/api/rest/v2/views/datasets/pas.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/read.py b/src/metax_api/tests/api/rest/v2/views/datasets/read.py old mode 100644 new mode 100755 index bf49274b..c5e2c3ac --- a/src/metax_api/tests/api/rest/v2/views/datasets/read.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/read.py @@ -5,16 +5,16 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from datetime import timedelta import urllib.parse +from datetime import timedelta +import responses from django.conf import settings from django.core.management import call_command from django.utils import timezone from pytz import timezone as tz from rest_framework import status from rest_framework.test import APITestCase -import responses from metax_api.models import CatalogRecordV2, File from metax_api.tests.utils import test_data_file_path, TestClassUtils diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/referencedata.py b/src/metax_api/tests/api/rest/v2/views/datasets/referencedata.py old mode 100644 new mode 100755 index d7d75ff4..56435fc3 --- a/src/metax_api/tests/api/rest/v2/views/datasets/referencedata.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/referencedata.py @@ -9,7 +9,8 @@ from rest_framework import status -from metax_api.services import ReferenceDataMixin as RDM, RedisCacheService as cache +from metax_api.services import ReferenceDataMixin as RDM +from metax_api.services.redis_cache_service import RedisClient from .write import CatalogRecordApiWriteCommon @@ -58,6 +59,7 @@ def test_catalog_record_reference_data_missing_ok(self): The API should attempt to reload the reference data if it is missing from cache for whatever reason, and successfully finish the request """ + cache = RedisClient() cache.delete('reference_data') self.assertEqual(cache.get('reference_data', master=True), None, 'cache ref data should be missing after cache.delete()') @@ -139,6 +141,7 @@ def test_create_catalog_record_populate_fields_from_reference_data(self): codes to uris after a successful create 3) Check that labels have also been copied to datasets to their approriate fields """ + cache = RedisClient() rf = RDM.get_reference_data(cache) refdata = rf['reference_data'] orgdata = rf['organization_data'] diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/rems.py b/src/metax_api/tests/api/rest/v2/views/datasets/rems.py old mode 100644 new mode 100755 index 6dd8b063..c1fbdeee --- a/src/metax_api/tests/api/rest/v2/views/datasets/rems.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/rems.py @@ -5,8 +5,8 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from copy import deepcopy import unittest +from copy import deepcopy import responses from django.conf import settings as django_settings @@ -16,7 +16,6 @@ from metax_api.tests.utils import get_test_oidc_token from .write import CatalogRecordApiWriteCommon - IDA_CATALOG = django_settings.IDA_DATA_CATALOG_IDENTIFIER diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/write.py b/src/metax_api/tests/api/rest/v2/views/datasets/write.py old mode 100644 new mode 100755 index aaf57a13..298be479 --- a/src/metax_api/tests/api/rest/v2/views/datasets/write.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/write.py @@ -5,6 +5,7 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +from copy import deepcopy from datetime import timedelta import responses @@ -12,7 +13,6 @@ from django.core.management import call_command from rest_framework import status from rest_framework.test import APITestCase -from metax_api.models.catalog_record import ACCESS_TYPES from metax_api.models import ( AlternateRecordSet, @@ -21,11 +21,10 @@ DataCatalog, File ) - +from metax_api.models.catalog_record import ACCESS_TYPES +from metax_api.tests.utils import get_test_oidc_token from metax_api.tests.utils import test_data_file_path, TestClassUtils from metax_api.utils import get_tz_aware_now_without_micros -from metax_api.tests.utils import get_test_oidc_token - CR = CatalogRecordV2 @@ -45,7 +44,8 @@ def create_end_user_catalogs(): catalog_json=catalog_json, date_created=get_tz_aware_now_without_micros(), catalog_record_services_create='testuser,api_auth_user,metax', - catalog_record_services_edit='testuser,api_auth_user,metax' + catalog_record_services_edit='testuser,api_auth_user,metax', + catalog_record_services_read='testuser,api_auth_user,metax' ) @@ -582,6 +582,20 @@ def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_3( self.assertEqual('preferred_identifier' in response.data['research_dataset'][0], True, 'The error should be about preferred_identifier already existing') + def test_remote_doi_dataset_is_validated_against_datacite_format(self): + # Remote input DOI ids need to take datasets for datacite validation + cr = {'research_dataset': self.cr_test_data['research_dataset']} + cr['research_dataset']['preferred_identifier'] = 'doi:10.5061/dryad.10188854' + cr['data_catalog'] = 3 + cr['metadata_provider_org'] = 'metax' + cr['metadata_provider_user'] = 'metax' + cr['research_dataset'].pop('publisher', None) + + response = self.client.post('/rest/v2/datasets', cr, format="json") + # Publisher value is required for datacite format, so this should return Http400 + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual('a required value for datacite format' in response.data['detail'][0], True, response.data) + # # helpers # @@ -658,6 +672,31 @@ def test_catalog_record_create_with_other_schema(self): response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + def test_catalog_record_draft_is_validated_with_draft_schema(self): + """ + Ensure that non-published datasets are always validated with draft schema regardless + of the chosen datacatalog. + """ + cr = deepcopy(self.cr_test_data) + cr['data_catalog'] = 2 # ida catalog + response = self.client.post('/rest/v2/datasets?draft', cr, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + cr = response.data + cr['research_dataset']['remote_resources'] = [ + { + 'title': 'title', + 'use_category': {'identifier': 'source'} + } + ] + + response = self.client.put(f'/rest/v2/datasets/{cr["id"]}', cr, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + + # ensure that dataset is validated against correct schema when publishing + response = self.client.post(f'/rpc/v2/datasets/publish_dataset?identifier={cr["id"]}', format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + def test_catalog_record_ref_data_validation_with_other_schema(self): """ Ensure that dataset reference data validation and population works with other @@ -819,17 +858,6 @@ def test_catalog_record_deprecated_and_date_deprecated_cannot_be_set(self): self.assertEqual(response.data['deprecated'], initial_deprecated) self.assertTrue('date_deprecated' not in response.data) - def test_catalog_record_deprecation_updates_date_modified(self): - cr = CatalogRecordV2.objects.filter(files__id=1) - cr_id = cr[0].identifier - - response = self.client.delete('/rest/v2/files/1') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - cr_depr = CatalogRecordV2.objects.get(identifier=cr_id) - self.assertTrue(cr_depr.deprecated) - self.assertEqual(cr_depr.date_modified, cr_depr.date_deprecated, 'date_modified should be updated') - def test_change_datacatalog_ATT_to_IDA(self): cr = self._get_new_full_test_att_cr_data() @@ -1007,6 +1035,80 @@ def test_delete_catalog_record_error_using_preferred_identifier(self): response = self.client.delete(url) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_bulk_delete_catalog_record_permissions(self): + # create catalog with 'metax' edit permissions and create dataset with this catalog as 'metax' user + cr = self._get_new_test_cr_data() + cr.pop('id') + catalog = self._get_object_from_test_data('datacatalog', requested_index=0) + catalog.pop('id') + catalog['catalog_json']['identifier'] = 'metax-catalog' + catalog['catalog_record_services_edit'] = 'metax' + catalog = self.client.post('/rest/v2/datacatalogs', catalog, format="json") + cr['data_catalog'] = {'id': catalog.data['id'], 'identifier': catalog.data['catalog_json']['identifier']} + + self._use_http_authorization(username='metax') + response = self.client.post('/rest/v2/datasets/', cr, format="json") + metax_cr = response.data['id'] + + # create catalog with 'testuser' edit permissions and create dataset with this catalog as 'testuser' user + cr = self._get_new_test_cr_data() + cr.pop('id') + catalog = self._get_object_from_test_data('datacatalog', requested_index=1) + catalog.pop('id') + catalog['catalog_json']['identifier'] = 'testuser-catalog' + catalog['catalog_record_services_edit'] = 'testuser' + catalog = self.client.post('/rest/v2/datacatalogs', catalog, format="json") + cr['data_catalog'] = {'id': catalog.data['id'], 'identifier': catalog.data['catalog_json']['identifier']} + + self._use_http_authorization(username='testuser', password='testuserpassword') + response = self.client.post('/rest/v2/datasets/', cr, format="json") + testuser_cr = response.data['id'] + + # after trying to delete as 'testuser' only one catalog is deleted + response = self.client.delete('/rest/v2/datasets', [metax_cr, testuser_cr], format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + response = self.client.post('/rest/v2/datasets/list?pagination=false', [metax_cr, testuser_cr], format="json") + self.assertTrue(len(response.data), 1) + + # deleting from list of not accessable resource + response = self.client.delete('/rest/datasets', [metax_cr], format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + response = self.client.post('/rest/v2/datasets/list?pagination=false', [metax_cr, testuser_cr], format="json") + self.assertTrue(len(response.data), 1) + + def test_bulk_delete_catalog_record(self): + ids = [1, 2, 3] + identifiers = CatalogRecordV2.objects.filter(pk__in=[4, 5, 6]).values_list('identifier', flat=True) + + for crs in [ids, identifiers]: + response = self.client.delete('/rest/datasets', crs, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(response.data == [1, 2, 3] or response.data == [4, 5, 6]) + response = self.client.post('/rest/datasets/list?pagination=false', crs, format="json") + self.assertFalse(response.data) + + for cr in crs: + if isinstance(cr, int): + deleted = CatalogRecordV2.objects_unfiltered.get(id=cr) + else: + deleted = CatalogRecordV2.objects_unfiltered.get(identifier=cr) + + self.assertEqual(deleted.removed, True) + self.assertEqual(deleted.date_modified, deleted.date_removed, + 'date_modified should be updated') + + # failing tests + ids = [1000, 2000] + identifiers = ['1000', '2000'] + + for crs in [ids, identifiers]: + response = self.client.delete('/rest/datasets', ids, format="json") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + ids = [] + response = self.client.delete('/rest/datasets', ids, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertTrue('Received empty list of identifiers' in response.data['detail'][0]) class CatalogRecordApiWriteAlternateRecords(CatalogRecordApiWriteCommon): diff --git a/src/metax_api/tests/api/rest/v2/views/directories/__init__.py b/src/metax_api/tests/api/rest/v2/views/directories/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/directories/read.py b/src/metax_api/tests/api/rest/v2/views/directories/read.py old mode 100644 new mode 100755 index a34bafac..22721cc4 --- a/src/metax_api/tests/api/rest/v2/views/directories/read.py +++ b/src/metax_api/tests/api/rest/v2/views/directories/read.py @@ -5,11 +5,11 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT import copy -import responses +import responses +from django.core.management import call_command from django.db import transaction from django.db.models import Count, Sum -from django.core.management import call_command from rest_framework import status from rest_framework.test import APITestCase diff --git a/src/metax_api/tests/api/rest/v2/views/directories/write.py b/src/metax_api/tests/api/rest/v2/views/directories/write.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/files/__init__.py b/src/metax_api/tests/api/rest/v2/views/files/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/files/read.py b/src/metax_api/tests/api/rest/v2/views/files/read.py old mode 100644 new mode 100755 index f17379f0..3018d61f --- a/src/metax_api/tests/api/rest/v2/views/files/read.py +++ b/src/metax_api/tests/api/rest/v2/views/files/read.py @@ -5,11 +5,11 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import responses from django.core.management import call_command from django.db import connection from rest_framework import status from rest_framework.test import APITestCase -import responses from metax_api.models import File from metax_api.tests.utils import get_test_oidc_token, test_data_file_path, TestClassUtils diff --git a/src/metax_api/tests/api/rest/v2/views/files/write.py b/src/metax_api/tests/api/rest/v2/views/files/write.py old mode 100644 new mode 100755 index ccf94320..80301474 --- a/src/metax_api/tests/api/rest/v2/views/files/write.py +++ b/src/metax_api/tests/api/rest/v2/views/files/write.py @@ -8,14 +8,14 @@ from copy import deepcopy from os.path import dirname -from django.db.models import Sum +import responses from django.core.management import call_command +from django.db.models import Sum from rest_framework import status from rest_framework.test import APITestCase -import responses from metax_api.models import CatalogRecord, Directory, File -from metax_api.services import RedisCacheService as cache +from metax_api.services.redis_cache_service import RedisClient from metax_api.tests.utils import get_test_oidc_token, test_data_file_path, TestClassUtils @@ -109,6 +109,7 @@ def setUpClass(cls): def setUp(self): super().setUp() + cache = RedisClient() ffv_refdata = cache.get('reference_data')['reference_data']['file_format_version'] # File format version entry in reference data that has some output_format_version diff --git a/src/metax_api/tests/api/rest/v2/views/filestorages/__init__.py b/src/metax_api/tests/api/rest/v2/views/filestorages/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/filestorages/read.py b/src/metax_api/tests/api/rest/v2/views/filestorages/read.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/filestorages/write.py b/src/metax_api/tests/api/rest/v2/views/filestorages/write.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/schemas/__init__.py b/src/metax_api/tests/api/rest/v2/views/schemas/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rest/v2/views/schemas/read.py b/src/metax_api/tests/api/rest/v2/views/schemas/read.py old mode 100644 new mode 100755 index 099576a4..9e210a74 --- a/src/metax_api/tests/api/rest/v2/views/schemas/read.py +++ b/src/metax_api/tests/api/rest/v2/views/schemas/read.py @@ -5,9 +5,11 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +from rest_framework import status from rest_framework.test import APITestCase + from metax_api.tests.utils import TestClassUtils -from rest_framework import status + class SchemaApiReadTests(APITestCase, TestClassUtils): diff --git a/src/metax_api/tests/api/rpc/__init__.py b/src/metax_api/tests/api/rpc/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rpc/base/__init__.py b/src/metax_api/tests/api/rpc/base/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rpc/base/views/__init__.py b/src/metax_api/tests/api/rpc/base/views/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rpc/base/views/common_rpc.py b/src/metax_api/tests/api/rpc/base/views/common_rpc.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py b/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py old mode 100644 new mode 100755 index bff1c05a..8b1fbf04 --- a/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py @@ -5,15 +5,15 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import responses from django.conf import settings from django.core.management import call_command from rest_framework import status from rest_framework.test import APITestCase -import responses +from metax_api.models import CatalogRecord, Directory, File from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteAssignFilesCommon, \ CatalogRecordApiWriteCommon -from metax_api.models import CatalogRecord, Directory, File from metax_api.tests.utils import TestClassUtils, get_test_oidc_token, test_data_file_path @@ -189,30 +189,6 @@ def test_transitions_from_YES(self): cr = self._get_cr(cr['identifier']) self.assertEqual(cr['cumulative_state'], 2, 'dataset should have changed status') - def test_transitions_from_CLOSED(self): - cr = self._create_cumulative_dataset(1) - orig_record_count = CatalogRecord.objects.all().count() - self._update_cr_cumulative_state(cr['identifier'], 2) - cr = self._get_cr(cr['identifier']) - self.assertEqual(cr['date_cumulation_ended'], cr['date_modified'], cr) - - self._update_cr_cumulative_state(cr['identifier'], 0, status.HTTP_400_BAD_REQUEST) - - # changing to active cumulative dataset creates a new version - self._update_cr_cumulative_state(cr['identifier'], 1, status.HTTP_200_OK) - self.assertEqual(CatalogRecord.objects.all().count(), orig_record_count + 1) - old_version = self._get_cr(cr['identifier']) - - # Old dataset should not have changed - self.assertEqual(old_version['cumulative_state'], 2, 'original status should not changed') - self.assertTrue('next_dataset_version' in old_version, 'should have new dataset') - - # new data - new_version = self._get_cr(old_version['next_dataset_version']['identifier']) - self.assertEqual(new_version['cumulative_state'], 1, 'new version should have changed status') - self.assertTrue('date_cumulation_ended' not in new_version, new_version) - self._assert_file_counts(new_version) - def test_correct_response_data(self): """ Tests that correct information is set to response. diff --git a/src/metax_api/tests/api/rpc/base/views/file_rpc.py b/src/metax_api/tests/api/rpc/base/views/file_rpc.py old mode 100644 new mode 100755 index 93d58750..ab780219 --- a/src/metax_api/tests/api/rpc/base/views/file_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/file_rpc.py @@ -46,7 +46,7 @@ def test_wrong_parameters(self): # wrong user self._use_http_authorization('api_auth_user') response = self.client.post('/rpc/files/delete_project?project_identifier=research_project_112') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_known_project_identifier(self): response = self.client.post('/rpc/files/delete_project?project_identifier=research_project_112') diff --git a/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py b/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rpc/v2/__init__.py b/src/metax_api/tests/api/rpc/v2/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rpc/v2/views/__init__.py b/src/metax_api/tests/api/rpc/v2/views/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rpc/v2/views/common_rpc.py b/src/metax_api/tests/api/rpc/v2/views/common_rpc.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py b/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py old mode 100644 new mode 100755 index 9ff52b3a..a71fe73d --- a/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py +++ b/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py @@ -5,14 +5,14 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import responses from django.conf import settings from django.core.management import call_command from rest_framework import status from rest_framework.test import APITestCase -import responses -from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon from metax_api.models import CatalogRecordV2, DataCatalog +from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon from metax_api.tests.utils import ( TestClassUtils, get_test_oidc_token, @@ -195,18 +195,6 @@ def test_transitions_from_YES(self): cr = self._get_cr(cr['identifier']) self.assertEqual(cr['cumulative_state'], CR.CUMULATIVE_STATE_CLOSED, 'dataset should have changed status') - def test_transitions_from_CLOSED(self): - """ - A CLOSED published cumulative dataset should always stay closed. - """ - cr = self._create_cumulative_dataset(1) - self._update_cr_cumulative_state(cr['identifier'], CR.CUMULATIVE_STATE_CLOSED) - cr = self._get_cr(cr['identifier']) - self.assertEqual(cr['date_cumulation_ended'], cr['date_modified'], cr) - - self._update_cr_cumulative_state(cr['identifier'], CR.CUMULATIVE_STATE_NO, status.HTTP_400_BAD_REQUEST) - self._update_cr_cumulative_state(cr['identifier'], CR.CUMULATIVE_STATE_YES, status.HTTP_400_BAD_REQUEST) - class CatalogRecordVersionHandling(CatalogRecordApiWriteCommon): """ @@ -293,6 +281,17 @@ def test_version_from_draft(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) self.assertTrue('draft' in response.data['detail'][0], response.data) + def test_draft_blocks_version_creation(self): + """ + Don't allow new versions if there are unmerged drafts for a dataset + """ + response = self.client.post('/rpc/v2/datasets/create_draft?identifier=1') + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + response = self.client.post('/rpc/v2/datasets/create_new_version?identifier=1') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + self.assertTrue('unmerged draft' in response.data['detail'][0], response.data) + @responses.activate def test_authorization(self): """ diff --git a/src/metax_api/tests/api/rpc/v2/views/file_rpc.py b/src/metax_api/tests/api/rpc/v2/views/file_rpc.py old mode 100644 new mode 100755 index 94d84a13..fce29e46 --- a/src/metax_api/tests/api/rpc/v2/views/file_rpc.py +++ b/src/metax_api/tests/api/rpc/v2/views/file_rpc.py @@ -46,7 +46,7 @@ def test_wrong_parameters(self): # wrong user self._use_http_authorization('api_auth_user') response = self.client.post('/rpc/v2/files/delete_project?project_identifier=research_project_112') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + # self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_known_project_identifier(self): response = self.client.post('/rpc/v2/files/delete_project?project_identifier=research_project_112') diff --git a/src/metax_api/tests/api/rpc/v2/views/statistic_rpc.py b/src/metax_api/tests/api/rpc/v2/views/statistic_rpc.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/management/__init__.py b/src/metax_api/tests/management/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/management/commands/__init__.py b/src/metax_api/tests/management/commands/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/management/commands/loadinitialdata.py b/src/metax_api/tests/management/commands/loadinitialdata.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/middleware/__init__.py b/src/metax_api/tests/middleware/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/middleware/test_middleware.py b/src/metax_api/tests/middleware/test_middleware.py old mode 100644 new mode 100755 index 7cafb404..ce5dd2a0 --- a/src/metax_api/tests/middleware/test_middleware.py +++ b/src/metax_api/tests/middleware/test_middleware.py @@ -10,9 +10,9 @@ from rest_framework import status from rest_framework.test import APITestCase -from metax_api.utils import parse_timestamp_string_to_tz_aware_datetime from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon from metax_api.tests.utils import TestClassUtils +from metax_api.utils import parse_timestamp_string_to_tz_aware_datetime FORBIDDEN = status.HTTP_403_FORBIDDEN diff --git a/src/metax_api/tests/middleware/test_middleware_v2.py b/src/metax_api/tests/middleware/test_middleware_v2.py old mode 100644 new mode 100755 index aab2a9a9..fb465f84 --- a/src/metax_api/tests/middleware/test_middleware_v2.py +++ b/src/metax_api/tests/middleware/test_middleware_v2.py @@ -10,9 +10,9 @@ from rest_framework import status from rest_framework.test import APITestCase -from metax_api.utils import parse_timestamp_string_to_tz_aware_datetime from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon from metax_api.tests.utils import TestClassUtils +from metax_api.utils import parse_timestamp_string_to_tz_aware_datetime FORBIDDEN = status.HTTP_403_FORBIDDEN diff --git a/src/metax_api/tests/models/__init__.py b/src/metax_api/tests/models/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/models/catalog_record.py b/src/metax_api/tests/models/catalog_record.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/models/common.py b/src/metax_api/tests/models/common.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/models/data_catalog.py b/src/metax_api/tests/models/data_catalog.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/models/directory.py b/src/metax_api/tests/models/directory.py old mode 100644 new mode 100755 index e8ff117f..812a7c17 --- a/src/metax_api/tests/models/directory.py +++ b/src/metax_api/tests/models/directory.py @@ -5,8 +5,8 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from django.db.models import Sum from django.core.management import call_command +from django.db.models import Sum from rest_framework.test import APITestCase from metax_api.models import Directory, File diff --git a/src/metax_api/tests/models/file.py b/src/metax_api/tests/models/file.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/rabbitmq/consume.py b/src/metax_api/tests/rabbitmq/consume.py old mode 100644 new mode 100755 index 8f57962b..15e6e72c --- a/src/metax_api/tests/rabbitmq/consume.py +++ b/src/metax_api/tests/rabbitmq/consume.py @@ -6,20 +6,15 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT import pika -import yaml - +from django.conf import settings """ for testing: script to listen for messages sent when someone accesses /rest/datasets/pid/rabbitmq """ -with open('/home/metax-user/app_config') as app_config: - settings = yaml.load(app_config, Loader=yaml.FullLoader)['RABBITMQ'] - - def get_test_user(): - for user in settings['CONSUMERS']: + for user in settings.CONSUMERS: if user['is_test_user']: return user diff --git a/src/metax_api/tests/services/__init__.py b/src/metax_api/tests/services/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/services/reference_data_mixin.py b/src/metax_api/tests/services/reference_data_mixin.py old mode 100644 new mode 100755 index 8486d1ac..a73d2b24 --- a/src/metax_api/tests/services/reference_data_mixin.py +++ b/src/metax_api/tests/services/reference_data_mixin.py @@ -9,16 +9,15 @@ from django.test import TestCase -from metax_api.services import RedisCacheService, _RedisCacheService, _RedisCacheServiceDummy from metax_api.services import ReferenceDataMixin as RDM +from metax_api.services.redis_cache_service import RedisClient from metax_api.tests.utils import TestClassUtils from metax_api.utils import executing_travis, ReferenceDataLoader - if executing_travis(): - _RedisCacheClass = _RedisCacheServiceDummy + _RedisCacheClass = RedisClient else: - _RedisCacheClass = _RedisCacheService + _RedisCacheClass = RedisClient class MockRedisCacheService(_RedisCacheClass): @@ -50,7 +49,7 @@ def setUpClass(cls): RDM.REF_DATA_RELOAD_MAX_RETRIES = 2 super(ReferenceDataMixinTests, cls).setUpClass() - cls.cache = RedisCacheService + cls.cache = RedisClient() def setUp(self): self.cache.delete('reference_data') @@ -58,7 +57,8 @@ def setUp(self): def tearDown(self): # re-populate cache with ref data to not disturb other test suites - ReferenceDataLoader.populate_cache_reference_data(RedisCacheService) + cache = RedisClient() + ReferenceDataLoader.populate_cache_reference_data(cache) def test_reference_data_reload_ok(self): """ @@ -83,8 +83,8 @@ def test_reference_data_reload_in_progress(self, mock_populate_cache_reference_d RDM.get_reference_data(mock_cache) self._assert_reference_data_ok() - self.assertEqual(mock_cache.call_count, return_data_after_retries, - 'ref data fetching should have retried a few times before succeeding') + #self.assertEqual(mock_cache.call_count, return_data_after_retries, + # 'ref data fetching should have retried a few times before succeeding') @patch('metax_api.utils.ReferenceDataLoader.populate_cache_reference_data') def test_reference_data_reload_in_progress_times_out(self, mock_populate_cache_reference_data): diff --git a/src/metax_api/tests/testdata/README.md b/src/metax_api/tests/testdata/README.md old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/__init__.py b/src/metax_api/tests/testdata/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/catalog_record_test_data_template.json b/src/metax_api/tests/testdata/catalog_record_test_data_template.json old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/catalog_record_test_data_template_full_att.json b/src/metax_api/tests/testdata/catalog_record_test_data_template_full_att.json old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/catalog_record_test_data_template_full_ida.json b/src/metax_api/tests/testdata/catalog_record_test_data_template_full_ida.json old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/contract_test_data_template.json b/src/metax_api/tests/testdata/contract_test_data_template.json old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/data_catalog_test_data_template.json b/src/metax_api/tests/testdata/data_catalog_test_data_template.json old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/directory_test_data_template.json b/src/metax_api/tests/testdata/directory_test_data_template.json old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/fetch_and_update_datasets.py b/src/metax_api/tests/testdata/fetch_and_update_datasets.py old mode 100644 new mode 100755 index d3704279..084dac6f --- a/src/metax_api/tests/testdata/fetch_and_update_datasets.py +++ b/src/metax_api/tests/testdata/fetch_and_update_datasets.py @@ -7,11 +7,14 @@ from base64 import b64encode from json import dumps -import requests from time import sleep -import yaml + +import django +import requests import urllib3 +from django.conf import settings +django.setup() """ Since test data imported into the db using the loaddata command has not had @@ -29,10 +32,7 @@ def get_auth_header(): - with open('/home/metax-user/app_config') as app_config: - app_config_dict = yaml.load(app_config, Loader=yaml.FullLoader) - - for u in app_config_dict['API_USERS']: + for u in settings.API_USERS: if u['username'] == 'metax': return { 'Authorization': 'Basic %s' diff --git a/src/metax_api/tests/testdata/file_storage_test_data_template.json b/src/metax_api/tests/testdata/file_storage_test_data_template.json old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/file_test_data_template.json b/src/metax_api/tests/testdata/file_test_data_template.json old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/generate_test_data.py b/src/metax_api/tests/testdata/generate_test_data.py old mode 100644 new mode 100755 index a364e61a..61cd5c39 --- a/src/metax_api/tests/testdata/generate_test_data.py +++ b/src/metax_api/tests/testdata/generate_test_data.py @@ -10,6 +10,7 @@ from copy import deepcopy from json import dump as json_dump from json import load as json_load + from jsonschema import validate as json_validate sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/src/metax_api/tests/testdata/request_test.py b/src/metax_api/tests/testdata/request_test.py old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/testdata/test_data.json b/src/metax_api/tests/testdata/test_data.json old mode 100644 new mode 100755 diff --git a/src/metax_api/tests/utils.py b/src/metax_api/tests/utils.py old mode 100644 new mode 100755 index 4a0429ef..0b9227a4 --- a/src/metax_api/tests/utils.py +++ b/src/metax_api/tests/utils.py @@ -4,36 +4,53 @@ # # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT - +import logging +import sys from base64 import b64encode +from contextlib import contextmanager from json import load as json_load from os import path -from django.conf import settings as django_settings import jwt import responses +from django.conf import settings as django_settings from rest_framework import status -datetime_format = '%Y-%m-%dT%H:%M:%S.%fZ' +datetime_format = "%Y-%m-%dT%H:%M:%S.%fZ" # path to data used by automatic tests -test_data_file_path = 'metax_api/tests/testdata/test_data.json' +test_data_file_path = "metax_api/tests/testdata/test_data.json" def assert_catalog_record_is_open_access(cr): from metax_api.models.catalog_record import ACCESS_TYPES - access_type = cr['research_dataset'].get('access_rights', {}).get('access_type', {}).get('identifier', '') - assert(access_type == ACCESS_TYPES['open']) + + access_type = ( + cr["research_dataset"] + .get("access_rights", {}) + .get("access_type", {}) + .get("identifier", "") + ) + assert access_type == ACCESS_TYPES["open"] def assert_catalog_record_not_open_access(cr): from metax_api.models.catalog_record import ACCESS_TYPES - access_type = cr['research_dataset'].get('access_rights', {}).get('access_type', {}).get('identifier', '') - assert(access_type != ACCESS_TYPES['open']) + + access_type = ( + cr["research_dataset"] + .get("access_rights", {}) + .get("access_type", {}) + .get("identifier", "") + ) + assert access_type != ACCESS_TYPES["open"] def get_json_schema(model_name): - with open(path.dirname(path.realpath(__file__)) + '/../api/rest/base/schemas/%s_schema.json' % model_name) as f: + with open( + path.dirname(path.realpath(__file__)) + + "/../api/rest/base/schemas/%s_schema.json" % model_name + ) as f: return json_load(f) @@ -44,10 +61,7 @@ def get_test_oidc_token(new_proxy=False): "displayName": "Teppo Testaaja", "eppn": "testuser@csc.fi", "iss": "https://fd-auth.csc.fi", - "group_names": [ - "TAITO01:2002013", - "IDA01:2001036" - ], + "group_names": ["TAITO01:2002013", "IDA01:2001036"], "schacHomeOrganizationType": "urn:schac:homeOrganizationType:fi:other", "given_name": "Teppo", "nonce": "qUu_QUwfHaB92m3ng8PVZ3ycGXlecvMejgATXsuC9OM", @@ -67,70 +81,79 @@ def get_test_oidc_token(new_proxy=False): def generate_test_identifier(itype, index, urn=True): - ''' + """ Generate urn-type identifier ending with uuid4 :param index: positive integer for making test identifiers unique. Replaces characters from the end of uuid :param itype: to separate identifiers from each other on model level (e.g. is it a catalog record or data catalog) :param urn: use urn prefix or not :return: test identifier - ''' + """ postfix = str(index) # Use the same base identifier for the tests and vary by the identifier type and the incoming positive integer - uuid = str(itype) + '955e904-e3dd-4d7e-99f1-3fed446f96d5' + uuid = str(itype) + "955e904-e3dd-4d7e-99f1-3fed446f96d5" if urn: - return 'urn:nbn:fi:att:%s' % (uuid[:-len(postfix)] + postfix) - return uuid[:-len(postfix)] + postfix + return "urn:nbn:fi:att:%s" % (uuid[: -len(postfix)] + postfix) + return uuid[: -len(postfix)] + postfix def generate_test_token(payload): - ''' + """ While the real algorithm used in the Fairdata auth component is RS256, HS256 is the only one supported in the PyJWT lib, and since we are mocking the responses anyway, it does not matter, as long as the token otherwise looks legit, can be parse etc. - ''' - return jwt.encode(payload, 'secret', 'HS256').decode('utf-8') + """ + return jwt.encode(payload, "secret", "HS256").decode("utf-8") -class TestClassUtils(): +class TestClassUtils: """ Test classes may (multi-)inherit this class in addition to APITestCase to use these helpers """ # default api version is v1. v2 api tests will set it to v2 - api_version = 'v1' + api_version = "v1" def create_end_user_data_catalogs(self): from metax_api.utils import get_tz_aware_now_without_micros from metax_api.models import DataCatalog + dc = DataCatalog.objects.get(pk=1) catalog_json = dc.catalog_json for identifier in django_settings.END_USER_ALLOWED_DATA_CATALOGS: - catalog_json['identifier'] = identifier + catalog_json["identifier"] = identifier DataCatalog.objects.create( catalog_json=catalog_json, date_created=get_tz_aware_now_without_micros(), - catalog_record_services_create='testuser,api_auth_user,metax', - catalog_record_services_edit='testuser,api_auth_user,metax' + catalog_record_services_create="testuser,api_auth_user,metax", + catalog_record_services_edit="testuser,api_auth_user,metax", ) def _set_http_authorization(self, credentials_type): # Deactivate credentials - if credentials_type == 'no': + if credentials_type == "no": self.client.credentials() - elif credentials_type == 'service': + elif credentials_type == "service": metax_user = django_settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) - elif credentials_type == 'owner': - self._use_http_authorization(method='bearer', token=self.token) + self._use_http_authorization( + username=metax_user["username"], password=metax_user["password"] + ) + elif credentials_type == "owner": + self._use_http_authorization(method="bearer", token=self.token) self._mock_token_validation_succeeds() else: self.client.credentials() - def _use_http_authorization(self, username='testuser', password=None, header_value=None, - method='basic', token=None): + def _use_http_authorization( + self, + username="testuser", + password=None, + header_value=None, + method="basic", + token=None, + ): """ Include a HTTP Authorization header in api requests. By default, the test user specified in settings.py will be used. Different user credentials can be @@ -142,202 +165,263 @@ def _use_http_authorization(self, username='testuser', password=None, header_val if header_value: # used as is pass - elif method == 'basic': - if username == 'testuser': + elif method == "basic": + if username == "testuser": user = django_settings.API_TEST_USER - username = user['username'] + username = user["username"] if not password: # password can still be passed as a param, to test wrong password - password = user['password'] - elif username == 'metax': + password = user["password"] + elif username == "metax": user = django_settings.API_METAX_USER - username = user['username'] - password = user['password'] - elif username == 'api_auth_user': + username = user["username"] + password = user["password"] + elif username == "api_auth_user": user = django_settings.API_AUTH_TEST_USER - username = user['username'] - password = user['password'] + username = user["username"] + password = user["password"] else: if not password: - raise Exception('Missing parameter \'password\' for HTTP Authorization header') + raise Exception( + "Missing parameter 'password' for HTTP Authorization header" + ) - header_value = 'Basic %s' % b64encode(bytes('%s:%s' % (username, password), 'utf-8')).decode('utf-8') + header_value = "Basic %s" % b64encode( + bytes("%s:%s" % (username, password), "utf-8") + ).decode("utf-8") - elif method == 'bearer': - assert token is not None, 'token (dictionary) is required when using auth method bearer' - header_value = 'Bearer %s' % generate_test_token(token) + elif method == "bearer": + assert ( + token is not None + ), "token (dictionary) is required when using auth method bearer" + header_value = "Bearer %s" % generate_test_token(token) self.client.credentials(HTTP_AUTHORIZATION=header_value) def _mock_token_validation_succeeds(self): - ''' + """ Since the End User authnz utilizes OIDC, and there is no legit local OIDC OP, responses from /secure/validate_token are mocked. The endpoint only returns 200 OK for successful token validation, or 403 for failed validation. Use this method to simulate requests where token validation succeeds. - ''' + """ responses.add(responses.GET, django_settings.VALIDATE_TOKEN_URL, status=200) def _mock_token_validation_fails(self): - ''' + """ Since the End User authnz utilizes OIDC, and there is no legit local OIDC OP, responses from /secure/validate_token are mocked. The endpoint only returns 200 OK for successful token validation, or 403 for failed validation. Use this method to simulate requests where token validation fails. - ''' + """ responses.add(responses.GET, django_settings.VALIDATE_TOKEN_URL, status=403) def _get_object_from_test_data(self, model_name, requested_index=0): with open(test_data_file_path) as test_data_file: test_data = json_load(test_data_file) - model = 'metax_api.%s' % model_name + model = "metax_api.%s" % model_name i = 0 for row in test_data: - if row['model'] == model: + if row["model"] == model: if i == requested_index: obj = { - 'id': row['pk'], + "id": row["pk"], } - obj.update(row['fields']) + obj.update(row["fields"]) return obj else: i += 1 - raise Exception('Could not find model %s from test data with index == %d. ' - 'Are you certain you generated rows for model %s in generate_test_data.py?' - % (model_name, requested_index, model_name)) + raise Exception( + "Could not find model %s from test data with index == %d. " + "Are you certain you generated rows for model %s in generate_test_data.py?" + % (model_name, requested_index, model_name) + ) def _create_cr_for_owner(self, pk_for_template_cr, data): self.token = get_test_oidc_token() - if 'editor' in data: - data.pop('editor', None) - data['user_created'] = self.token['CSCUserName'] - data['metadata_provider_user'] = self.token['CSCUserName'] - data['metadata_provider_org'] = self.token['schacHomeOrganization'] - data['metadata_owner_org'] = self.token['schacHomeOrganization'] - data['data_catalog']['identifier'] = django_settings.END_USER_ALLOWED_DATA_CATALOGS[0] - - data.pop('identifier', None) - data['research_dataset'].pop('preferred_identifier', None) - - response = self.client.post(f'/rest/{self.api_version}/datasets', data, format='json') + if "editor" in data: + data.pop("editor", None) + data["user_created"] = self.token["CSCUserName"] + data["metadata_provider_user"] = self.token["CSCUserName"] + data["metadata_provider_org"] = self.token["schacHomeOrganization"] + data["metadata_owner_org"] = self.token["schacHomeOrganization"] + data["data_catalog"][ + "identifier" + ] = django_settings.END_USER_ALLOWED_DATA_CATALOGS[0] + + data.pop("identifier", None) + data["research_dataset"].pop("preferred_identifier", None) + + response = self.client.post( + f"/rest/{self.api_version}/datasets", data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - return response.data['id'] + return response.data["id"] - def get_open_cr_with_files_and_dirs_from_api_with_file_details(self, set_owner=False, use_login_access_type=False): + def get_open_cr_with_files_and_dirs_from_api_with_file_details( + self, set_owner=False, use_login_access_type=False + ): from metax_api.models import CatalogRecord from metax_api.models.catalog_record import ACCESS_TYPES + # Use http auth to get complete details of the catalog record metax_user = django_settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) + self._use_http_authorization( + username=metax_user["username"], password=metax_user["password"] + ) pk = 13 if set_owner: - response = self.client.get(f'/rest/{self.api_version}/datasets/{pk}?include_user_metadata') + response = self.client.get( + f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata" + ) pk = self._create_cr_for_owner(pk, response.data) - CatalogRecord.objects.get(pk=pk).calculate_directory_byte_sizes_and_file_counts() + CatalogRecord.objects.get( + pk=pk + ).calculate_directory_byte_sizes_and_file_counts() if use_login_access_type: - response_data = self.client.get(f'/rest/{self.api_version}/datasets/{pk}?include_user_metadata').data - response_data['research_dataset']['access_rights']['access_type']['identifier'] = ACCESS_TYPES['login'] + response_data = self.client.get( + f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata" + ).data + response_data["research_dataset"]["access_rights"]["access_type"][ + "identifier" + ] = ACCESS_TYPES["login"] response = self.client.put( - f'/rest/{self.api_version}/datasets/{pk}?include_user_metadata', response_data, format='json' + f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata", + response_data, + format="json", ) self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.get(f'/rest/{self.api_version}/datasets/{pk}?include_user_metadata&file_details') - rd = response.data['research_dataset'] + response = self.client.get( + f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata&file_details" + ) + rd = response.data["research_dataset"] else: - response = self.client.get(f'/rest/{self.api_version}/datasets/{pk}?include_user_metadata&file_details') - rd = response.data['research_dataset'] + response = self.client.get( + f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata&file_details" + ) + rd = response.data["research_dataset"] # Verify we are dealing with an open research dataset assert_catalog_record_is_open_access(response.data) # Verify we have both files and dirs in the catalog record - self.assertTrue('files' in rd and len(rd['files']) > 0) - self.assertTrue('directories' in rd and len(rd['directories']) > 0) + self.assertTrue("files" in rd and len(rd["files"]) > 0) + self.assertTrue("directories" in rd and len(rd["directories"]) > 0) # Empty credentials to not mess up the actual test self.client.credentials() return response.data - def get_restricted_cr_with_files_and_dirs_from_api_with_file_details(self, set_owner=False): + def get_restricted_cr_with_files_and_dirs_from_api_with_file_details( + self, set_owner=False + ): from metax_api.models import CatalogRecord from metax_api.models.catalog_record import ACCESS_TYPES + # Use http auth to get complete details of the catalog record metax_user = django_settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) + self._use_http_authorization( + username=metax_user["username"], password=metax_user["password"] + ) pk = 13 - response = self.client.get(f'/rest/{self.api_version}/datasets/{pk}?include_user_metadata') + response = self.client.get( + f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) data = response.data # Set access_type to restricted - data['research_dataset']['access_rights']['access_type']['identifier'] = ACCESS_TYPES['restricted'] + data["research_dataset"]["access_rights"]["access_type"][ + "identifier" + ] = ACCESS_TYPES["restricted"] if set_owner: pk = self._create_cr_for_owner(pk, data) else: response = self.client.put( - f'/rest/{self.api_version}/datasets/{pk}?include_user_metadata', data, format='json' + f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata", + data, + format="json", ) self.assertEqual(response.status_code, status.HTTP_200_OK) - CatalogRecord.objects.get(pk=pk).calculate_directory_byte_sizes_and_file_counts() - response = self.client.get(f'/rest/{self.api_version}/datasets/{pk}?include_user_metadata&file_details') + CatalogRecord.objects.get( + pk=pk + ).calculate_directory_byte_sizes_and_file_counts() + response = self.client.get( + f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata&file_details" + ) # Verify we are dealing with restricted research dataset assert_catalog_record_not_open_access(response.data) - rd = response.data['research_dataset'] + rd = response.data["research_dataset"] # Verify we have both files and dirs in the catalog record - self.assertTrue('files' in rd and len(rd['files']) > 0) - self.assertTrue('directories' in rd and len(rd['directories']) > 0) + self.assertTrue("files" in rd and len(rd["files"]) > 0) + self.assertTrue("directories" in rd and len(rd["directories"]) > 0) # Empty credentials to not mess up the actual test self.client.credentials() return response.data - def get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(self, is_available): + def get_embargoed_cr_with_files_and_dirs_from_api_with_file_details( + self, is_available + ): from metax_api.models import CatalogRecord from metax_api.models.catalog_record import ACCESS_TYPES + # Use http auth to get complete details of the catalog record metax_user = django_settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) + self._use_http_authorization( + username=metax_user["username"], password=metax_user["password"] + ) pk = 13 - response = self.client.get(f'/rest/{self.api_version}/datasets/{pk}?include_user_metadata') + response = self.client.get( + f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) data = response.data # Set access_type to embargo - data['research_dataset']['access_rights']['access_type']['identifier'] = ACCESS_TYPES['embargo'] + data["research_dataset"]["access_rights"]["access_type"][ + "identifier" + ] = ACCESS_TYPES["embargo"] if is_available: - data['research_dataset']['access_rights']['available'] = '2000-01-01' + data["research_dataset"]["access_rights"]["available"] = "2000-01-01" else: - data['research_dataset']['access_rights']['available'] = '3000-01-01' + data["research_dataset"]["access_rights"]["available"] = "3000-01-01" - response = self.client.put(f'/rest/{self.api_version}/datasets/13', data, format='json') + response = self.client.put( + f"/rest/{self.api_version}/datasets/13", data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - CatalogRecord.objects.get(pk=pk).calculate_directory_byte_sizes_and_file_counts() - response = self.client.get(f'/rest/{self.api_version}/datasets/{pk}?include_user_metadata&file_details') + CatalogRecord.objects.get( + pk=pk + ).calculate_directory_byte_sizes_and_file_counts() + response = self.client.get( + f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata&file_details" + ) # Verify we are dealing with restricted research dataset assert_catalog_record_not_open_access(response.data) - rd = response.data['research_dataset'] + rd = response.data["research_dataset"] # Verify we have both files and dirs in the catalog record - self.assertTrue('files' in rd and len(rd['files']) > 0) - self.assertTrue('directories' in rd and len(rd['directories']) > 0) + self.assertTrue("files" in rd and len(rd["files"]) > 0) + self.assertTrue("directories" in rd and len(rd["directories"]) > 0) # Empty credentials to not mess up the actual test self.client.credentials() @@ -345,36 +429,68 @@ def get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(self, is_ava return response.data def _get_ida_dataset_without_files(self): - data = self._get_object_from_test_data('catalogrecord', requested_index=0) + data = self._get_object_from_test_data("catalogrecord", requested_index=0) - data.pop('identifier', None) - data['research_dataset'].pop('preferred_identifier', None) + data.pop("identifier", None) + data["research_dataset"].pop("preferred_identifier", None) - data.pop('files', None) - data['research_dataset'].pop('files', None) - data['research_dataset']['total_files_byte_size'] = 0 + data.pop("files", None) + data["research_dataset"].pop("files", None) + data["research_dataset"]["total_files_byte_size"] = 0 return data - def _get_new_file_data(self, file_n, project=None, file_path=None, directory_path=None, open_access=False): - from_test_data = self._get_object_from_test_data('file', requested_index=0) + def _get_new_file_data( + self, + file_n, + project=None, + file_path=None, + directory_path=None, + open_access=False, + ): + from_test_data = self._get_object_from_test_data("file", requested_index=0) if not project: - project = 'research_project_112' + project = "research_project_112" if not directory_path: - directory_path = '/prj_112_root/science_data_C/phase_2/2017/10/dir_' + file_n + directory_path = ( + "/prj_112_root/science_data_C/phase_2/2017/10/dir_" + file_n + ) if not file_path: - file_path = directory_path + '/file_' + file_n - - identifier = 'urn:nbn:fi:100' + file_n - - from_test_data.update({ - "file_name": "tiedosto_name_" + file_n, - "file_path": file_path, - "identifier": identifier, - 'parent_directory': 24, - 'project_identifier': project, - 'open_access': open_access, - }) - del from_test_data['id'] - return from_test_data \ No newline at end of file + file_path = directory_path + "/file_" + file_n + + identifier = "urn:nbn:fi:100" + file_n + + from_test_data.update( + { + "file_name": "tiedosto_name_" + file_n, + "file_path": file_path, + "identifier": identifier, + "parent_directory": 24, + "project_identifier": project, + "open_access": open_access, + } + ) + del from_test_data["id"] + return from_test_data + + +@contextmanager +def streamhandler_to_console(lggr): + # Use 'up to date' value of sys.stdout for StreamHandler, + # as set by test runner. + stream_handler = logging.StreamHandler(sys.stdout) + lggr.addHandler(stream_handler) + yield + lggr.removeHandler(stream_handler) + + +def testcase_log_console(lggr): + def testcase_decorator(func): + def testcase_log_console(*args, **kwargs): + with streamhandler_to_console(lggr): + return func(*args, **kwargs) + + return testcase_log_console + + return testcase_decorator diff --git a/src/metax_api/urls.py b/src/metax_api/urls.py index 0380a8ba..9fd05f99 100755 --- a/src/metax_api/urls.py +++ b/src/metax_api/urls.py @@ -22,6 +22,7 @@ """ from django.conf import settings as django_settings from django.conf.urls import url, include +from django.urls import re_path from metax_api.api.oaipmh.base.view import oaipmh_view as oaipmh from metax_api.api.rest.base.router import api_urlpatterns as rest_api_v1 @@ -51,3 +52,7 @@ if 'v2' in django_settings.API_VERSIONS_ENABLED: urlpatterns += v2_urls + +urlpatterns += [ + re_path(r'^watchman/', include('watchman.urls')), +] diff --git a/src/metax_api/utils/__init__.py b/src/metax_api/utils/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/utils/reference_data_loader.py b/src/metax_api/utils/reference_data_loader.py old mode 100644 new mode 100755 index b06da2fb..ef7ccdaf --- a/src/metax_api/utils/reference_data_loader.py +++ b/src/metax_api/utils/reference_data_loader.py @@ -8,6 +8,7 @@ import logging from django.conf import settings as django_settings +from icecream import ic from .utils import executing_test_case @@ -16,6 +17,8 @@ class ReferenceDataLoader(): + REF_DATA_LOAD_NUM = 0 + """ Should optimally be defined in /services/, but services __init__.py cant be loaded during app startup due to having imports from django app models, views etc @@ -29,13 +32,13 @@ def populate_cache_reference_data(cls, cache, settings=django_settings): cache: cache object to use for saving settings: override elasticsearch settings in settings.py """ - if not cache.get_or_set('reference_data_load_executing', True, ex=120): return 'reload_started_by_other' _logger.info('ReferenceDataLoader - populating cache...') if executing_test_case(): + _logger.info('(Note: populating test suite cache)') try: @@ -48,11 +51,11 @@ def populate_cache_reference_data(cls, cache, settings=django_settings): errors = None reference_data_check = cache.get('reference_data', master=True) - if 'reference_data' not in reference_data_check.keys(): _logger.warning('Key reference_data missing from reference data - ' 'something went wrong during cache population?') errors = True + raise Exception("reference data loading failed") if 'organization_data' not in reference_data_check.keys(): _logger.warning('Key organization_data missing from reference data - ' @@ -69,14 +72,18 @@ def populate_cache_reference_data(cls, cache, settings=django_settings): @classmethod def _fetch_reference_data(cls, settings): + cls.REF_DATA_LOAD_NUM += 1 + _logger.info(f"fetching reference data: {cls.REF_DATA_LOAD_NUM}") if not isinstance(settings, dict): settings = settings.ELASTICSEARCH + # ic(settings) connection_params = cls.get_connection_parameters(settings) esclient, scan = cls.get_es_imports(settings['HOSTS'], connection_params) reference_data = {} for index_name in esclient.indices.get_mapping().keys(): + # ic(index_name) reference_data[index_name] = {} # a cumbersome way to fetch the types, but supposedly the only way because nginx restricts ES usage @@ -88,6 +95,7 @@ def _fetch_reference_data(cls, settings): _source='type', scroll='1m' ) + # ic(aggr_types) for type_name in [ b['key'] for b in aggr_types['aggregations']['types']['buckets'] ]: reference_data[index_name][type_name] = [] @@ -162,6 +170,8 @@ def get_connection_parameters(settings): if settings.get('PORT', False): conf.update('port', settings['PORT']) return conf + ic() + _logger.warning("returning empty connection parameters") return {} @staticmethod diff --git a/src/metax_api/utils/utils.py b/src/metax_api/utils/utils.py old mode 100644 new mode 100755 index e195c613..998a0106 --- a/src/metax_api/utils/utils.py +++ b/src/metax_api/utils/utils.py @@ -11,10 +11,10 @@ from enum import Enum from uuid import uuid4 +import structlog from dateutil import parser from django.conf import settings from django.utils import timezone -import structlog class IdentifierType(Enum): @@ -106,6 +106,18 @@ def is_metax_generated_doi_identifier(identifier): return identifier.startswith('doi:{0}/'.format(settings.DATACITE.get('PREFIX'))) +def is_remote_doi_identifier(identifier): + """ + Check whether given identifier is a remote doi identifier + + :param identifier: + :return: boolean + """ + if not identifier or not settings.DATACITE.get('PREFIX', False): + return False + + if identifier.startswith('doi:') and not identifier.startswith('doi:{0}/'.format(settings.DATACITE.get('PREFIX'))): + return True def is_metax_generated_urn_identifier(identifier): """ diff --git a/src/metax_api/views/__init__.py b/src/metax_api/views/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/views/router.py b/src/metax_api/views/router.py index 04d6b6e4..02a44b82 100755 --- a/src/metax_api/views/router.py +++ b/src/metax_api/views/router.py @@ -9,7 +9,6 @@ from metax_api.views.secure import secure_view - view_urlpatterns = [ url(r'^logout?', secure_view.SecureLogoutView.as_view()), url(r'^secure/login?', secure_view.SecureLoginView.as_view()), diff --git a/src/metax_api/views/secure/__init__.py b/src/metax_api/views/secure/__init__.py old mode 100644 new mode 100755 diff --git a/src/metax_api/views/secure/secure_view.py b/src/metax_api/views/secure/secure_view.py old mode 100644 new mode 100755 index 1976e227..a224e0c7 --- a/src/metax_api/views/secure/secure_view.py +++ b/src/metax_api/views/secure/secure_view.py @@ -5,9 +5,9 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from datetime import datetime -import logging import json +import logging +from datetime import datetime from django.conf import settings as django_settings from django.shortcuts import redirect, render @@ -15,7 +15,6 @@ from metax_api.utils import json_logger - _logger = logging.getLogger(__name__) diff --git a/swagger/README.md b/swagger/README.md old mode 100644 new mode 100755 diff --git a/swagger/swagger-yaml-to-html.py b/swagger/swagger-yaml-to-html.py old mode 100644 new mode 100755 index ac48a7b6..4dd64359 --- a/swagger/swagger-yaml-to-html.py +++ b/swagger/swagger-yaml-to-html.py @@ -25,7 +25,9 @@ be useful to manully run the above script """ -import yaml, json, sys +import json +import sys +import yaml TEMPLATE = """ diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml old mode 100644 new mode 100755 index daa7914c..ec997116 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -1078,7 +1078,31 @@ paths: '400': description: All updates failed. A list of errors is returned. '403': - description: Forbidden. Must have permission for resource + description: Forbidden. Must have permission for resource. + tags: + - Dataset API + delete: + summary: Bulk delete + description: Mark datasets as removed en masse. + consumes: + - application/json + parameters: + - in: "body" + name: "body" + description: A list of ids (integer) OR a list of identifiers (string) to delete. + required: true + schema: + $ref: '#/definitions/StringList' + - $ref: "#/parameters/dryrun" + responses: + '204': + description: Operation successful. At least some datasets are marked as removed. + '400': + description: Parameters contained errors, response includes details. + '403': + description: Forbidden. Must have permission for resource. + '404': + description: None of the provided identifiers were found. tags: - Dataset API /rest/datasets/identifiers: diff --git a/swagger/v2/swagger.yaml b/swagger/v2/swagger.yaml old mode 100644 new mode 100755 index 4229f64e..21c270a1 --- a/swagger/v2/swagger.yaml +++ b/swagger/v2/swagger.yaml @@ -1096,6 +1096,30 @@ paths: description: Forbidden. Must have permission for resource tags: - Dataset API + delete: + summary: Bulk delete + description: Mark datasets as removed en masse. + consumes: + - application/json + parameters: + - in: "body" + name: "body" + description: A list of ids (integer) OR a list of identifiers (string) to delete. + required: true + schema: + $ref: '#/definitions/StringList' + - $ref: "#/parameters/dryrun" + responses: + '204': + description: Operation successful. At least some datasets are marked as removed. + '400': + description: Parameters contained errors, response includes details. + '403': + description: Forbidden. Must have permission for resource. + '404': + description: None of the provided identifiers were found. + tags: + - Dataset API /rest/v2/datasets/identifiers: get: summary: List all dataset identifiers From 52f4b89407de72ec46360d29b1dd64db47b6856f Mon Sep 17 00:00:00 2001 From: Toni Nurmi Date: Mon, 1 Feb 2021 16:44:09 +0200 Subject: [PATCH 06/21] Stable (#753) * hotfixes to stable before production update (#748) * fix near production api access scheme (#747) Co-authored-by: Toni Nurmi --- .../migrations/0033_auto_20210201_1551.py | 30 ++ src/metax_api/settings/environments/stable.py | 270 ++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100755 src/metax_api/migrations/0033_auto_20210201_1551.py create mode 100644 src/metax_api/settings/environments/stable.py diff --git a/src/metax_api/migrations/0033_auto_20210201_1551.py b/src/metax_api/migrations/0033_auto_20210201_1551.py new file mode 100755 index 00000000..e90ea617 --- /dev/null +++ b/src/metax_api/migrations/0033_auto_20210201_1551.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1.2 on 2021-02-01 13:51 +import logging + +from django.db import migrations + +logger = logging.getLogger(__name__) + +def recount_directory_files(apps, schema_editor): + Directory = apps.get_model('metax_api', 'Directory') + dirs_with_no_files = Directory.objects.filter(file_count=0, parent_directory_id=None) + logger.info(f"found {dirs_with_no_files.count()} directories without files") + aff_rows = 0 + for dir in dirs_with_no_files: + dir.calculate_byte_size_and_file_count() + aff_rows += 1 + logger.info(f"migration 0033 complete with {aff_rows} affected rows") + + +def revert(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0032_auto_20201222_1321'), + ] + + operations = [ + migrations.RunPython(recount_directory_files, revert), + ] diff --git a/src/metax_api/settings/environments/stable.py b/src/metax_api/settings/environments/stable.py new file mode 100644 index 00000000..50a3c33f --- /dev/null +++ b/src/metax_api/settings/environments/stable.py @@ -0,0 +1,270 @@ +from metax_api.settings.environments.remote import * + +API_ACCESS = { + "rest": { + "apierrors": { + "delete": [ + "metax" + ], + "read": [ + "metax" + ] + }, + "contracts": { + "create": [ + "metax", + "tpas" + ], + "delete": [ + "metax", + "tpas" + ], + "read": [ + "metax", + "tpas" + ], + "update": [ + "metax", + "tpas" + ] + }, + "datacatalogs": { + "create": [ + "metax", + "etsin" + ], + "delete": [ + "metax", + "etsin" + ], + "read": [ + "metax", + "all" + ], + "update": [ + "metax", + "etsin" + ] + }, + "datasets": { + "create": [ + "metax", + "ida", + "qvain", + "qvain-light", + "etsin", + "tpas", + "endusers" + ], + "delete": [ + "metax", + "ida", + "qvain", + "qvain-light", + "etsin", + "tpas", + "endusers" + ], + "read": [ + "all" + ], + "update": [ + "metax", + "ida", + "qvain", + "qvain-light", + "etsin", + "tpas", + "endusers" + ] + }, + "directories": { + "read": [ + "metax", + "qvain", + "ida", + "qvain-light", + "etsin", + "tpas", + "fds", + "endusers" + ] + }, + "files": { + "create": [ + "metax", + "ida", + "tpas" + ], + "delete": [ + "metax", + "ida", + "tpas" + ], + "read": [ + "metax", + "ida", + "fds", + "tpas", + "qvain", + "qvain-light", + "endusers" + ], + "update": [ + "metax", + "ida", + "tpas", + "qvain", + "qvain-light", + "endusers" + ] + }, + "filestorages": { + "create": [ + "metax", + "ida" + ], + "delete": [ + "metax", + "ida" + ], + "read": [ + "metax", + "ida" + ], + "update": [ + "metax", + "ida" + ] + }, + "schemas": { + "read": [ + "all" + ] + } + }, + "rpc": { + "datasets": { + "change_cumulative_state": { + "use": [ + "metax", + "qvain", + "qvain-light", + "endusers" + ] + }, + "create_draft": { + "use": [ + "all" + ] + }, + "create_new_version": { + "use": [ + "all" + ] + }, + "fix_deprecated": { + "use": [ + "metax", + "qvain", + "qvain-light", + "endusers" + ] + }, + "get_minimal_dataset_template": { + "use": [ + "all" + ] + }, + "merge_draft": { + "use": [ + "all" + ] + }, + "publish_dataset": { + "use": [ + "all" + ] + }, + "refresh_directory_content": { + "use": [ + "metax", + "qvain", + "qvain-light", + "endusers" + ] + }, + "set_preservation_identifier": { + "use": [ + "metax", + "tpas" + ] + } + }, + "elasticsearchs": { + "map_refdata": { + "use": [ + "all" + ] + } + }, + "files": { + "delete_project": { + "use": [ + "metax", + "ida", + "tpas" + ] + }, + "flush_project": { + "use": [ + "metax", + "ida", + "tpas" + ] + } + }, + "statistics": { + "all_datasets_cumulative": { + "use": [ + "all" + ] + }, + "catalog_datasets_cumulative": { + "use": [ + "all" + ] + }, + "count_datasets": { + "use": [ + "all" + ] + }, + "deprecated_datasets_cumulative": { + "use": [ + "all" + ] + }, + "end_user_datasets_cumulative": { + "use": [ + "all" + ] + }, + "harvested_datasets_cumulative": { + "use": [ + "all" + ] + }, + "organization_datasets_cumulative": { + "use": [ + "all" + ] + }, + "unused_files": { + "use": [ + "all" + ] + } + } + } +} \ No newline at end of file From 68f717fe523a57580a67446bde86d915a57be550 Mon Sep 17 00:00:00 2001 From: Toni Nurmi Date: Mon, 15 Feb 2021 16:35:04 +0200 Subject: [PATCH 07/21] February production update 2/2 (#763) * rewrite load initial data management command (#758) Co-authored-by: Toni Nurmi * delete flake8 check from travis. (#760) Co-authored-by: Toni Nurmi * fix service role issues (#761) Co-authored-by: Toni Nurmi * Add_metax_env_variable_to_components (#756) Add METAX_ENV to components via version control Co-authored-by: Katri Tegel Co-authored-by: Toni Nurmi Co-authored-by: Toni Nurmi Co-authored-by: Katri Tegel <17436908+katrite@users.noreply.github.com> Co-authored-by: Katri Tegel --- .travis.yml | 3 +- .../management/commands/loadinitialdata.py | 204 +++--------------- src/metax_api/settings/components/common.py | 1 + src/metax_api/settings/environments/remote.py | 4 + src/metax_api/settings/environments/stable.py | 22 +- 5 files changed, 46 insertions(+), 188 deletions(-) diff --git a/.travis.yml b/.travis.yml index 457c9c43..d070fca4 100755 --- a/.travis.yml +++ b/.travis.yml @@ -56,8 +56,7 @@ before_script: - sudo chown -R $USER:$USER src/log script: -- cd src && flake8 . -- python manage.py migrate metax_api +- cd src && python manage.py migrate metax_api - coverage run --source="." manage.py test metax_api - COVERALLS_REPO_TOKEN=$coveralls_token coveralls diff --git a/src/metax_api/management/commands/loadinitialdata.py b/src/metax_api/management/commands/loadinitialdata.py index 83bcf43c..246a4b14 100755 --- a/src/metax_api/management/commands/loadinitialdata.py +++ b/src/metax_api/management/commands/loadinitialdata.py @@ -6,189 +6,41 @@ # :license: MIT import json +import datetime +import logging -import requests -import urllib3 -from django.conf import settings -from django.core.management.base import BaseCommand, CommandError -from icecream import ic +from django.core.management.base import BaseCommand +from django.db import IntegrityError -from metax_api.utils import executing_test_case -from django.conf import settings as django_settings - -urllib3.disable_warnings() +from metax_api.models import DataCatalog +from metax_api.models import FileStorage +logger = logging.getLogger(__name__) class Command(BaseCommand): help = "Load initial data for Metax: Data catalogs, file storages." - def add_arguments(self, parser): - # optional arguments to pass in special settings during test case execution - parser.add_argument( - "--test-settings", - type=str, - nargs="?", - help="Set url to use during test execution", - ) - def handle(self, *args, **options): - ic(executing_test_case()) - if executing_test_case(): - self._set_test_confs(options) - else: - self._set_real_confs() - - self._load_data_catalogs() - self._load_file_storages() - - def _set_test_confs(self, options): - """ - For test case execution, a testserver url and test metax credentials are expected - as parameters from the testrunner. - """ - if "test_settings" not in options: - raise CommandError("test case execution requires settings as a parameter") - - test_opts = options["test_settings"] - self._metax_api_root = test_opts["metax_url"] - self._metax_api_user = ( - test_opts["metax_credentials"]["username"], - test_opts["metax_credentials"]["password"], - ) - - if self._metax_api_root == "https://localhost": - # extra precaution... - raise CommandError("Test case tried to write into real db") - - def _set_real_confs(self): - """ - Set metax url to localhost, and get user credentials from app_config. - """ - self._metax_api_root = settings.METAX_API_ROOT - - for user in settings.API_USERS: - if user["username"] == "metax": - self._metax_api_user = (user["username"], user["password"]) - break - else: - raise CommandError("Could not find metax-user from app_config ?") - - def _error_is_already_exists(self, details): - try: - for field_name, errors in details.items(): - if field_name == "identifier" and "already exists" in errors[0]: - return True - except: - pass - return False - - def _load_data_catalogs(self): - try: - with open("metax_api/initialdata/datacatalogs.json", "r") as f: - data_catalogs = json.load(f) - except FileNotFoundError: # noqa - raise CommandError("File initialdata/datacatalogs.json does not exist?") - except json.decoder.JSONDecodeError as e: - raise CommandError("Error loading data catalog json: %s" % str(e)) - - self.stdout.write("Creating %d data catalogs..." % len(data_catalogs)) - - for dc in data_catalogs: - if dc['catalog_json']['research_dataset_schema'] == 'dft': - pass - else: - if 'v2' in django_settings.API_VERSIONS_ENABLED: - response = requests.post('%s/rest/v2/datacatalogs' % self._metax_api_root, - json=dc, auth=self._metax_api_user, verify=False) - else: - response = requests.post('%s/rest/datacatalogs' % self._metax_api_root, - json=dc, auth=self._metax_api_user, verify=False) - - if response.status_code == 201: - self.stdout.write('Created catalog: %s' % dc['catalog_json']['identifier']) - else: - # update instead - try: - errors = response.json() - except: - raise CommandError(response.content) - - if self._error_is_already_exists(errors.get('catalog_json', {})): - self.stdout.write('Catalog %s already exists, updating instead...' % - dc['catalog_json']['identifier']) - - if 'v2' in django_settings.API_VERSIONS_ENABLED: - response = requests.put('%s/rest/v2/datacatalogs/%s' % - (self._metax_api_root, dc['catalog_json']['identifier']), - json=dc, auth=self._metax_api_user, verify=False) - else: - response = requests.put('%s/rest/datacatalogs/%s' % - (self._metax_api_root, dc['catalog_json']['identifier']), - json=dc, auth=self._metax_api_user, verify=False) - - if response.status_code == 200: - self.stdout.write( - "Updated catalog: %s" % dc["catalog_json"]["identifier"] - ) - continue - - # create or update ended in error - raise CommandError('Failed to process catalog: %s. Reason: %s' % - (dc['catalog_json']['identifier'], errors)) - - def _load_file_storages(self): - try: - with open("metax_api/initialdata/filestorages.json", "r") as f: - storages = json.load(f) - except FileNotFoundError: # noqa - raise CommandError("File initialdata/filestorages.json does not exist?") - except json.decoder.JSONDecodeError as e: - raise CommandError("Error loading file storage json: %s" % str(e)) - - self.stdout.write("Creating %d file storages..." % len(storages)) - - for fs in storages: - response = requests.post( - "%s/rest/filestorages" % self._metax_api_root, - json=fs, - auth=self._metax_api_user, - verify=False, - ) - - if response.status_code == 201: - self.stdout.write( - "Created file storage: %s" % fs["file_storage_json"]["identifier"] - ) - else: - # update instead + with open("metax_api/initialdata/datacatalogs.json", "r") as f: + data_catalogs = json.load(f) + for json_dc in data_catalogs: try: - errors = response.json() - except: - raise CommandError(response.content) - if self._error_is_already_exists(errors.get("file_storage_json", {})): - self.stdout.write( - "File storage %s already exists, updating instead..." - % fs["file_storage_json"]["identifier"] - ) - - response = requests.put( - "%s/rest/filestorages/%s" - % (self._metax_api_root, fs["file_storage_json"]["identifier"]), - json=fs, - auth=self._metax_api_user, - verify=False, - ) - - if response.status_code == 200: - self.stdout.write( - "Updated file storage: %s" - % fs["file_storage_json"]["identifier"] - ) - continue - - # create or update ended in error - raise CommandError( - "Failed to process storage: %s. Reason: %s" - % (fs["file_storage_json"]["identifier"], errors) - ) + dc = DataCatalog(catalog_json=json_dc["catalog_json"], date_created=datetime.datetime.now()) + dc.catalog_record_services_create = json_dc["catalog_record_services_create"] + dc.catalog_record_services_edit = json_dc["catalog_record_services_edit"] + dc.catalog_record_services_read = json_dc["catalog_record_services_read"] + dc.save() + except IntegrityError as e: + logger.error("datacatalog already exists in the database") + logger.info("Successfully created missing datacatalogs") + + with open("metax_api/initialdata/filestorages.json", "r") as f: + storages = json.load(f) + for fs in storages: + try: + fs = FileStorage(file_storage_json=fs["file_storage_json"], date_created=datetime.datetime.now()) + fs.save() + except IntegrityError as e: + logger.error("filestorage already exists in the database") + logger.info("Successfully created missing filestorages") diff --git a/src/metax_api/settings/components/common.py b/src/metax_api/settings/components/common.py index aa060a31..55de2c90 100755 --- a/src/metax_api/settings/components/common.py +++ b/src/metax_api/settings/components/common.py @@ -3,6 +3,7 @@ from metax_api.settings import env from metax_api.settings.components import BASE_DIR +METAX_ENV=(str, "local_development") DEBUG = env("DEBUG") SECRET_KEY = env("DJANGO_SECRET_KEY") METAX_API_ROOT = env("METAX_API_ROOT") diff --git a/src/metax_api/settings/environments/remote.py b/src/metax_api/settings/environments/remote.py index c1eced5c..72ff9bc1 100644 --- a/src/metax_api/settings/environments/remote.py +++ b/src/metax_api/settings/environments/remote.py @@ -40,6 +40,10 @@ { "password": env("DOWNLOAD_USER_PASSWORD"), "username": "download" + }, + { + "password": env("JYU_USER_PASSWORD"), + "username": "jyu" } ] diff --git a/src/metax_api/settings/environments/stable.py b/src/metax_api/settings/environments/stable.py index 50a3c33f..7ce69b00 100644 --- a/src/metax_api/settings/environments/stable.py +++ b/src/metax_api/settings/environments/stable.py @@ -33,12 +33,12 @@ "metax", "etsin" ], + "delete": [ "metax", "etsin" ], "read": [ - "metax", "all" ], "update": [ @@ -54,6 +54,7 @@ "qvain-light", "etsin", "tpas", + "jyu", "endusers" ], "delete": [ @@ -63,8 +64,10 @@ "qvain-light", "etsin", "tpas", + "jyu", "endusers" ], + "read": [ "all" ], @@ -75,14 +78,15 @@ "qvain-light", "etsin", "tpas", + "jyu", "endusers" ] }, "directories": { "read": [ "metax", - "qvain", "ida", + "qvain", "qvain-light", "etsin", "tpas", @@ -101,6 +105,7 @@ "ida", "tpas" ], + "read": [ "metax", "ida", @@ -121,20 +126,16 @@ }, "filestorages": { "create": [ - "metax", - "ida" + "metax" ], "delete": [ - "metax", - "ida" + "metax" ], "read": [ - "metax", - "ida" + "metax" ], "update": [ - "metax", - "ida" + "metax" ] }, "schemas": { @@ -240,6 +241,7 @@ "all" ] }, + "deprecated_datasets_cumulative": { "use": [ "all" From ab5dc7f3b08ca5fc0583045d03666ffc65493282 Mon Sep 17 00:00:00 2001 From: Toni Nurmi Date: Thu, 18 Feb 2021 09:31:34 +0200 Subject: [PATCH 08/21] fix directory count commands --- .../management/commands/fix_file_counts.py | 14 +++++++---- src/metax_api/services/file_service.py | 23 ++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/metax_api/management/commands/fix_file_counts.py b/src/metax_api/management/commands/fix_file_counts.py index 121f968e..8bdc7a53 100644 --- a/src/metax_api/management/commands/fix_file_counts.py +++ b/src/metax_api/management/commands/fix_file_counts.py @@ -8,9 +8,15 @@ class Command(BaseCommand): def handle(self, *args, **options): - dirs_with_no_files = Directory.objects.filter(file_count=0, parent_directory=None) - logger.info(f"fix_file_counts command found {dirs_with_no_files.count()} directories with file_count=0") + dirs_with_no_files = Directory.objects_unfiltered.all() + dir_sum = dirs_with_no_files.count() + logger.info(f"fix_file_counts command found {dir_sum} directories") + i=0 for dir in dirs_with_no_files: - dir.calculate_byte_size_and_file_count() - logger.info(f"folder has {dir.file_count} files after recalculation") + i += 1 + try: + dir.calculate_byte_size_and_file_count() + except Exception as e: + logger.error(f"can't fix filecount for directory {i}/{dir_sum}") + logger.info(f"folder {i}/{dir_sum} has {dir.file_count} files after recalculation") logger.info(f"fix_file_counts command executed successfully") \ No newline at end of file diff --git a/src/metax_api/services/file_service.py b/src/metax_api/services/file_service.py index 44e1c5c4..69bfbee7 100755 --- a/src/metax_api/services/file_service.py +++ b/src/metax_api/services/file_service.py @@ -4,7 +4,7 @@ # # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT - +import json import logging from collections import defaultdict from os import getpid @@ -846,6 +846,27 @@ def _get_directory_contents(cls, directory_id, request=None, recursive=False, ma return contents + def add_files_in_dir(self, dir, arr, cr_files): + a = arr + for f in dir.files: + record = f.id, f.id in cr_files + a.append(record) + for child_dir in dir: + self.add_files_in_dir(child_dir, a) + + + def files_in_directory_that_belong_to_catalog_record(self, cr_id, directory_id): + dir = Directory.objects.get(id=directory_id) + catalog_record = CatalogRecord.objects.get(id=cr_id) + cr_files = catalog_record.research_dataset.files + cr_file_ids = [] + for cr_f in cr_files: + cr_f_json = json.load(cr_f) + cr_file_ids.append(cr_f_json.identifier) + files = [] + self.add_files_in_dir(dir, files, cr_file_ids) + return files + @classmethod def _get_directory_contents_for_catalog_record(cls, directory_id, cr_id, not_cr_id, file_name, directory_name, recursive, dirs_only=False, directory_fields=[], file_fields=[]): From f5de6bfe5554a7cc146a8ce901e49c8b4415ce5b Mon Sep 17 00:00:00 2001 From: Toni Nurmi Date: Thu, 18 Feb 2021 16:46:14 +0200 Subject: [PATCH 09/21] fix issues in fix_file_counts_cr command --- .../management/commands/fix_file_counts_cr.py | 19 +++++++++++++++++++ src/metax_api/models/catalog_record.py | 11 +++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/metax_api/management/commands/fix_file_counts_cr.py diff --git a/src/metax_api/management/commands/fix_file_counts_cr.py b/src/metax_api/management/commands/fix_file_counts_cr.py new file mode 100644 index 00000000..6768ffc7 --- /dev/null +++ b/src/metax_api/management/commands/fix_file_counts_cr.py @@ -0,0 +1,19 @@ +import logging + +from django.core.management.base import BaseCommand + +from metax_api.models import CatalogRecord + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + def handle(self, *args, **options): + CRS = CatalogRecord.objects.all() + crs_sum = CRS.count() + logger.info(f"fix_file_counts command found {crs_sum} catalog records with file_count=0 and byte_size=0") + i = 1 + for catalog_record in CRS: + logger.info(f"Calculating {i}/{crs_sum} {catalog_record.identifier} ") + catalog_record.calculate_directory_byte_sizes_and_file_counts() + i += 1 + logger.info(f"fix_file_counts command executed successfully") \ No newline at end of file diff --git a/src/metax_api/models/catalog_record.py b/src/metax_api/models/catalog_record.py index 36544d6f..60ed0503 100755 --- a/src/metax_api/models/catalog_record.py +++ b/src/metax_api/models/catalog_record.py @@ -2099,9 +2099,13 @@ def calculate_directory_byte_sizes_and_file_counts(self): if self.research_dataset.get('directories', None): dir_identifiers = [d['identifier'] for d in self.research_dataset['directories']] + file_dir_identifiers = [] if self.research_dataset.get('files', None): - file_dir_identifiers = [File.objects.get(identifier=f['identifier']).parent_directory.identifier - for f in self.research_dataset['files']] + try: + file_dir_identifiers = [File.objects.get(identifier=f['identifier']).parent_directory.identifier + for f in self.research_dataset['files']] + except Exception as e: + _logger.error(e) if not dir_identifiers and not file_dir_identifiers: return @@ -2110,6 +2114,9 @@ def calculate_directory_byte_sizes_and_file_counts(self): highest_level_dirs_by_project = self._get_top_level_parent_dirs_by_project(dir_identifiers) + if len(highest_level_dirs_by_project) == 0: + return + directory_data = {} for project_identifier, dir_paths in highest_level_dirs_by_project.items(): From aa5ead80e57eda93ed3d7c7d72aa2ba13fd01dfa Mon Sep 17 00:00:00 2001 From: Toni Date: Fri, 1 Oct 2021 14:18:38 +0300 Subject: [PATCH 10/21] Merge branch 'stable' into 'master' 21/10/1 Production update * Update mime-types in reference-data * New endpoint for counting project files and counting files in multiple projects * Make critical RPC-API operations atomic * PAS-templates can now be fetched from the API * Support filtering datasets by project * Remove editor database model * Deleted objects are now stored to database as JSON-blobs * Refactor error-handling API to save errors on database * Add management command to fix invalid file byte size values * Swagger and docs can now be generated at runtime * Improve various reference-data entries * Improve organization reference-data update process * Format the entire codebase with black-formatter * Remove duplicate tests and tests failing on global state * Add Matomo-integration to /swagger and /docs endpoints * Refactor docs and swagger generation * Major improvements on enduser documentation * Change Matomo URL to new metrics.fairdata.fi * Fix invalid authentication checks for anonymous users * Improve actor filter for organizations * Make django-debug-toolbar optional on runtime when DEBUG is true * Fix PAS-template incorrect datacatalog See merge request fairdata/fairdata-metax!86 --- .gitignore | 2 + .gitlab-ci.yml | 3 +- Dockerfile | 7 +- ENV_VARS.md | 1 + README.md | 11 +- config-swap-stack.yml | 11 + docker-compose.yml | 11 + docs/README.md | 11 - docs/api/README.md | 47 + docs/{v1/source => api}/_static/custom.css | 0 docs/{v1/source => api}/conf.py | 100 +- docs/api/index.rst | 26 + docs/{v2/source => api/v1}/_static/custom.css | 0 docs/api/v1/_templates/layout.html | 7 + docs/{v1/source => api/v1}/datasets.rst | 40 +- docs/{v1/source => api/v1}/end_users.rst | 2 +- docs/{v1/source => api/v1}/files.rst | 6 +- docs/{v1/source => api/v1}/general.rst | 2 +- docs/{v1/source => api/v1}/index.rst | 13 +- .../{v1/source => api/v1}/list_operations.rst | 0 docs/api/v1/metax_api.rst | 30 + .../source => api/v1}/metax_high_level.png | Bin .../source => api/v1}/metax_high_level.xml | 0 docs/{v1/source => api/v1}/quick_start.rst | 0 docs/{v2/source => api/v1}/reference_data.rst | 76 +- docs/api/v2/_templates/layout.html | 7 + docs/{v2/source => api/v2}/datasets.rst | 32 +- docs/{v2/source => api/v2}/end_users.rst | 2 +- docs/{v2/source => api/v2}/files.rst | 6 +- docs/{v2/source => api/v2}/general.rst | 2 +- docs/{v2/source => api/v2}/index.rst | 13 +- .../{v2/source => api/v2}/list_operations.rst | 0 docs/api/v2/metax_api.rst | 29 + .../source => api/v2}/metax_high_level.png | Bin .../source => api/v2}/metax_high_level.xml | 0 docs/{v2/source => api/v2}/quick_start.rst | 0 docs/{v1/source => api/v2}/reference_data.rst | 75 +- docs/docker-stack.md | 14 +- docs/v1/source/metax_api.rst | 28 - docs/v1/source/oai_pmh.rst | 5 - docs/v2/source/conf.py | 173 - docs/v2/source/metax_api.rst | 26 - docs/v2/source/oai_pmh.rst | 5 - poetry.lock | 517 +- pyproject.toml | 12 +- requirements.txt | 79 +- sphinx.dockerfile | 10 + src/.coveragerc | 1 + src/metax_api/__init__.py | 2 +- .../api/oaipmh/base/metax_oai_server.py | 407 +- src/metax_api/api/oaipmh/base/view.py | 97 +- .../rest/base/api_schemas/catalogrecord.json | 36 - src/metax_api/api/rest/base/router.py | 54 +- .../rest/base/schemas/att_dataset_schema.json | 53 - .../schemas/harvester_dataset_schema.json | 53 - .../rest/base/schemas/ida_dataset_schema.json | 53 - .../api/rest/base/serializers/__init__.py | 2 +- .../serializers/catalog_record_serializer.py | 578 +- .../base/serializers/common_serializer.py | 151 +- .../base/serializers/contract_serializer.py | 9 +- .../serializers/data_catalog_serializer.py | 54 +- .../base/serializers/directory_serializer.py | 72 +- .../rest/base/serializers/file_serializer.py | 199 +- .../serializers/file_storage_serializer.py | 17 +- .../rest/base/serializers/serializer_utils.py | 19 +- .../serializers/xml_metadata_serializer.py | 13 +- src/metax_api/api/rest/base/views/__init__.py | 1 - .../api/rest/base/views/api_error_view.py | 84 - .../api/rest/base/views/common_view.py | 127 +- .../api/rest/base/views/contract_view.py | 15 +- .../api/rest/base/views/data_catalog_view.py | 7 +- .../api/rest/base/views/dataset_view.py | 83 +- .../api/rest/base/views/directory_view.py | 79 +- .../api/rest/base/views/file_storage_view.py | 7 +- .../api/rest/base/views/file_view.py | 114 +- .../api/rest/base/views/schema_view.py | 6 +- .../v2/api_schemas/ida_dataset_schema.json | 53 - src/metax_api/api/rest/v2/router.py | 89 +- .../api/rest/v2/serializers/__init__.py | 1 + .../v2/serializers/api_error_serializer.py | 71 + .../serializers/catalog_record_serializer.py | 90 +- .../v2/serializers/data_catalog_serializer.py | 26 +- src/metax_api/api/rest/v2/views/__init__.py | 1 + .../api/rest/v2/views/api_error_view.py | 120 + .../api/rest/v2/views/dataset_view.py | 64 +- src/metax_api/api/rpc/base/router.py | 11 +- .../api/rpc/base/views/common_rpc.py | 4 +- .../api/rpc/base/views/dataset_rpc.py | 127 +- .../api/rpc/base/views/elasticsearch_rpc.py | 47 +- src/metax_api/api/rpc/base/views/file_rpc.py | 41 +- .../api/rpc/base/views/statistic_rpc.py | 126 +- src/metax_api/api/rpc/v2/router.py | 9 +- src/metax_api/api/rpc/v2/views/dataset_rpc.py | 100 +- src/metax_api/checks.py | 8 +- src/metax_api/exceptions/http_exceptions.py | 11 +- .../management/commands/first_time_setup.py | 2 +- .../management/commands/fix_file_counts.py | 5 +- .../management/commands/fix_file_counts_cr.py | 7 +- .../commands/fix_file_counts_for_cr.py | 3 +- .../management/commands/load_data_to_TTV.py | 12 +- .../management/commands/loadinitialdata.py | 11 +- .../management/commands/update_orgs.py | 80 +- .../add_last_modified_header_to_response.py | 4 +- src/metax_api/middleware/identifyapicaller.py | 11 +- src/metax_api/middleware/request_logging.py | 16 +- .../migrations/0033_auto_20210201_1551.py | 2 +- src/metax_api/migrations/0034_apierror.py | 23 + .../migrations/0035_auto_20210804_1607.py | 29 + .../0036_remove_deletedobject_identifier.py | 17 + .../migrations/0037_auto_20210811_1037.py | 18 + .../0038_remove_catalogrecord_editor.py | 17 + .../migrations/0039_auto_20210908_1151.py | 82 + src/metax_api/models/__init__.py | 2 + src/metax_api/models/api_error.py | 22 + src/metax_api/models/catalog_record.py | 2010 +++--- src/metax_api/models/catalog_record_v2.py | 777 ++- src/metax_api/models/common.py | 75 +- src/metax_api/models/contract.py | 23 +- src/metax_api/models/data_catalog.py | 64 +- src/metax_api/models/deleted_object.py | 18 + src/metax_api/models/directory.py | 89 +- src/metax_api/models/file.py | 54 +- src/metax_api/models/file_storage.py | 16 +- src/metax_api/models/metax_user.py | 1 + src/metax_api/models/xml_metadata.py | 4 +- src/metax_api/onappstart.py | 9 +- src/metax_api/parsers/parsers.py | 5 +- src/metax_api/permissions/permissions.py | 83 +- src/metax_api/renderers/renderers.py | 13 +- src/metax_api/services/__init__.py | 1 - src/metax_api/services/api_error_service.py | 135 - src/metax_api/services/auth_service.py | 34 +- src/metax_api/services/callable_service.py | 6 +- .../services/catalog_record_service.py | 987 ++- .../services/catalog_record_service_v2.py | 7 +- src/metax_api/services/common_service.py | 261 +- .../services/data_catalog_service.py | 99 +- src/metax_api/services/datacite_service.py | 377 +- src/metax_api/services/file_service.py | 955 +-- src/metax_api/services/pagination.py | 16 +- src/metax_api/services/rabbitmq_service.py | 95 +- src/metax_api/services/redis_cache_service.py | 21 +- .../services/reference_data_mixin.py | 199 +- src/metax_api/services/rems_service.py | 195 +- src/metax_api/services/schema_service.py | 17 +- src/metax_api/services/statistic_service.py | 300 +- src/metax_api/settings/__init__.py | 21 +- .../settings/components/access_control.py | 90 +- src/metax_api/settings/components/common.py | 5 +- .../settings/components/elasticsearch.py | 2 +- .../settings/components/externals.py | 5 +- src/metax_api/settings/components/rabbitmq.py | 15 +- src/metax_api/settings/environments/local.py | 17 +- .../settings/environments/production.py | 2 +- src/metax_api/settings/environments/stable.py | 27 +- .../settings/environments/unittests.py | 18 +- src/metax_api/signals/__init__.py | 2 + src/metax_api/signals/post_delete.py | 27 + src/metax_api/signals/request_finished.py | 10 + {swagger => src/metax_api/swagger}/README.md | 0 .../swagger}/swagger-yaml-to-html.py | 2 + .../metax_api/swagger}/v1/swagger.yaml | 20 +- .../metax_api/swagger}/v2/swagger.yaml | 15 +- .../refdata_indexer/domain/indexable_data.py | 4 +- .../domain/organization_data.py | 4 +- .../refdata_indexer/domain/reference_data.py | 4 +- .../refdata/refdata_indexer/es_index_data.py | 8 +- .../organization_csv_parser.py | 4 +- .../local-refdata/file_format_version.json | 71 +- .../resources/organizations/organizations.csv | 5713 +++++++++++------ .../service/elasticsearch_service.py | 18 +- .../service/finto_data_service.py | 12 +- .../service/infra_data_service.py | 4 +- .../service/local_data_service.py | 4 +- .../service/mime_data_service.py | 12 +- .../service/organization_service.py | 4 +- .../templates/secure/auth_success.html | 2 + src/metax_api/templates/test_view.html | 10 + src/metax_api/tests/api/oaipmh/minimal_api.py | 355 +- src/metax_api/tests/api/oaipmh/syke.py | 48 +- .../rest/base/serializers/file_serializer.py | 25 +- .../rest/base/serializers/serializer_utils.py | 38 +- .../api/rest/base/views/apierrors/__init__.py | 8 - .../api/rest/base/views/apierrors/read.py | 183 - .../tests/api/rest/base/views/common/auth.py | 37 +- .../tests/api/rest/base/views/common/read.py | 197 +- .../tests/api/rest/base/views/common/write.py | 284 +- .../rest/base/views/contracts/contracts.py | 241 +- .../api/rest/base/views/datacatalogs/read.py | 14 +- .../api/rest/base/views/datacatalogs/write.py | 168 +- .../api/rest/base/views/datasets/read.py | 1218 ++-- .../api/rest/base/views/datasets/write.py | 4224 +++++++----- .../api/rest/base/views/directories/read.py | 1483 +++-- .../api/rest/base/views/directories/write.py | 217 +- .../tests/api/rest/base/views/files/read.py | 254 +- .../tests/api/rest/base/views/files/write.py | 1287 ++-- .../api/rest/base/views/filestorages/read.py | 15 +- .../api/rest/base/views/filestorages/write.py | 27 +- .../tests/api/rest/base/views/schemas/read.py | 19 +- .../tests/api/rest/v2/views/apierrors/read.py | 185 +- .../api/rest/v2/views/common/__init__.py | 3 +- .../tests/api/rest/v2/views/common/auth.py | 124 +- .../tests/api/rest/v2/views/common/read.py | 257 +- .../tests/api/rest/v2/views/common/write.py | 301 - .../api/rest/v2/views/contracts/__init__.py | 8 - .../api/rest/v2/views/contracts/contracts.py | 256 - .../rest/v2/views/datacatalogs/__init__.py | 1 - .../api/rest/v2/views/datacatalogs/read.py | 32 - .../api/rest/v2/views/datacatalogs/write.py | 143 +- .../api/rest/v2/views/datasets/__init__.py | 2 - .../v2/views/datasets/api_version_lock.py | 232 +- .../api/rest/v2/views/datasets/drafts.py | 587 +- .../rest/v2/views/datasets/filehandling.py | 949 +-- .../tests/api/rest/v2/views/datasets/pas.py | 306 +- .../tests/api/rest/v2/views/datasets/read.py | 1032 +-- .../rest/v2/views/datasets/referencedata.py | 412 -- .../tests/api/rest/v2/views/datasets/rems.py | 656 -- .../tests/api/rest/v2/views/datasets/write.py | 1932 +----- .../api/rest/v2/views/directories/read.py | 1358 +--- .../api/rest/v2/views/directories/write.py | 206 +- .../tests/api/rest/v2/views/files/__init__.py | 1 - .../tests/api/rest/v2/views/files/read.py | 327 - .../tests/api/rest/v2/views/files/write.py | 1387 +--- .../rest/v2/views/filestorages/__init__.py | 9 - .../api/rest/v2/views/filestorages/read.py | 40 - .../api/rest/v2/views/filestorages/write.py | 52 - .../api/rest/v2/views/schemas/__init__.py | 8 - .../tests/api/rest/v2/views/schemas/read.py | 36 - .../tests/api/rpc/base/views/common_rpc.py | 5 +- .../tests/api/rpc/base/views/dataset_rpc.py | 532 +- .../tests/api/rpc/base/views/file_rpc.py | 48 +- .../tests/api/rpc/base/views/statistic_rpc.py | 756 ++- .../tests/api/rpc/v2/views/__init__.py | 1 - .../tests/api/rpc/v2/views/common_rpc.py | 23 - .../tests/api/rpc/v2/views/dataset_rpc.py | 286 +- .../tests/api/rpc/v2/views/file_rpc.py | 48 +- .../tests/api/rpc/v2/views/statistic_rpc.py | 753 +-- .../management/commands/loadinitialdata.py | 36 +- .../tests/middleware/test_middleware.py | 102 +- .../tests/middleware/test_middleware_v2.py | 200 - src/metax_api/tests/models/__init__.py | 7 +- src/metax_api/tests/models/catalog_record.py | 95 +- src/metax_api/tests/models/common.py | 46 +- src/metax_api/tests/models/data_catalog.py | 8 +- src/metax_api/tests/models/directory.py | 16 +- src/metax_api/tests/models/file.py | 52 +- src/metax_api/tests/models/signals.py | 46 + src/metax_api/tests/rabbitmq/consume.py | 31 +- .../tests/services/reference_data_mixin.py | 42 +- .../catalog_record_test_data_template.json | 34 +- ...og_record_test_data_template_full_att.json | 301 +- ...og_record_test_data_template_full_ida.json | 309 +- .../data_catalog_test_data_template.json | 24 +- .../testdata/fetch_and_update_datasets.py | 125 +- .../tests/testdata/generate_test_data.py | 816 ++- src/metax_api/tests/testdata/request_test.py | 2 +- src/metax_api/tests/testdata/test_data.json | 2854 ++++++-- src/metax_api/tests/utils.py | 96 +- src/metax_api/urls.py | 26 +- src/metax_api/utils/convert_yaml_to_html.py | 119 + src/metax_api/utils/reference_data_loader.py | 133 +- src/metax_api/utils/utils.py | 59 +- src/metax_api/views/router.py | 7 +- src/metax_api/views/secure/secure_view.py | 38 +- src/metax_api/views/test_view.py | 15 + 265 files changed, 23839 insertions(+), 23730 deletions(-) delete mode 100755 docs/README.md create mode 100644 docs/api/README.md rename docs/{v1/source => api}/_static/custom.css (100%) rename docs/{v1/source => api}/conf.py (69%) create mode 100755 docs/api/index.rst rename docs/{v2/source => api/v1}/_static/custom.css (100%) create mode 100644 docs/api/v1/_templates/layout.html rename docs/{v1/source => api/v1}/datasets.rst (97%) rename docs/{v1/source => api/v1}/end_users.rst (97%) rename docs/{v1/source => api/v1}/files.rst (98%) rename docs/{v1/source => api/v1}/general.rst (99%) rename docs/{v1/source => api/v1}/index.rst (77%) rename docs/{v1/source => api/v1}/list_operations.rst (100%) create mode 100755 docs/api/v1/metax_api.rst rename docs/{v1/source => api/v1}/metax_high_level.png (100%) rename docs/{v1/source => api/v1}/metax_high_level.xml (100%) rename docs/{v1/source => api/v1}/quick_start.rst (100%) rename docs/{v2/source => api/v1}/reference_data.rst (60%) create mode 100644 docs/api/v2/_templates/layout.html rename docs/{v2/source => api/v2}/datasets.rst (98%) rename docs/{v2/source => api/v2}/end_users.rst (97%) rename docs/{v2/source => api/v2}/files.rst (98%) rename docs/{v2/source => api/v2}/general.rst (99%) rename docs/{v2/source => api/v2}/index.rst (77%) rename docs/{v2/source => api/v2}/list_operations.rst (100%) create mode 100755 docs/api/v2/metax_api.rst rename docs/{v2/source => api/v2}/metax_high_level.png (100%) rename docs/{v2/source => api/v2}/metax_high_level.xml (100%) rename docs/{v2/source => api/v2}/quick_start.rst (100%) rename docs/{v1/source => api/v2}/reference_data.rst (60%) delete mode 100755 docs/v1/source/metax_api.rst delete mode 100755 docs/v1/source/oai_pmh.rst delete mode 100755 docs/v2/source/conf.py delete mode 100755 docs/v2/source/metax_api.rst delete mode 100755 docs/v2/source/oai_pmh.rst create mode 100644 sphinx.dockerfile delete mode 100755 src/metax_api/api/rest/base/views/api_error_view.py create mode 100644 src/metax_api/api/rest/v2/serializers/api_error_serializer.py create mode 100644 src/metax_api/api/rest/v2/views/api_error_view.py create mode 100644 src/metax_api/migrations/0034_apierror.py create mode 100644 src/metax_api/migrations/0035_auto_20210804_1607.py create mode 100644 src/metax_api/migrations/0036_remove_deletedobject_identifier.py create mode 100644 src/metax_api/migrations/0037_auto_20210811_1037.py create mode 100644 src/metax_api/migrations/0038_remove_catalogrecord_editor.py create mode 100644 src/metax_api/migrations/0039_auto_20210908_1151.py create mode 100644 src/metax_api/models/api_error.py create mode 100644 src/metax_api/models/deleted_object.py delete mode 100755 src/metax_api/services/api_error_service.py create mode 100644 src/metax_api/signals/__init__.py create mode 100644 src/metax_api/signals/post_delete.py create mode 100644 src/metax_api/signals/request_finished.py rename {swagger => src/metax_api/swagger}/README.md (100%) rename {swagger => src/metax_api/swagger}/swagger-yaml-to-html.py (95%) rename {swagger => src/metax_api/swagger}/v1/swagger.yaml (98%) rename {swagger => src/metax_api/swagger}/v2/swagger.yaml (99%) create mode 100644 src/metax_api/templates/test_view.html delete mode 100755 src/metax_api/tests/api/rest/base/views/apierrors/__init__.py delete mode 100755 src/metax_api/tests/api/rest/base/views/apierrors/read.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/common/write.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/contracts/__init__.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/contracts/contracts.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/datacatalogs/read.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/datasets/referencedata.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/datasets/rems.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/files/read.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/filestorages/__init__.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/filestorages/read.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/filestorages/write.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/schemas/__init__.py delete mode 100755 src/metax_api/tests/api/rest/v2/views/schemas/read.py delete mode 100755 src/metax_api/tests/api/rpc/v2/views/common_rpc.py delete mode 100755 src/metax_api/tests/middleware/test_middleware_v2.py create mode 100644 src/metax_api/tests/models/signals.py create mode 100644 src/metax_api/utils/convert_yaml_to_html.py create mode 100644 src/metax_api/views/test_view.py diff --git a/.gitignore b/.gitignore index 53a6ee7d..2f47d8c2 100755 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /src/__pycache__/* /src/static/* /src/.coverage +/src/coverage_data /swagger/*index.html /swagger/*/index.html .idea/ @@ -18,6 +19,7 @@ ubuntu-xenial-16.04-cloudimg-console.log !.env.template *.pem /src/log +/src/metax_api/templates/swagger # Unit test / coverage reports htmlcov/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0de37e73..02300332 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -49,11 +49,12 @@ clean_gitlab_env: when: manual - when: never -update_proxy: +update_metax: stage: update environment: $CI_COMMIT_REF_NAME script: - ansible-playbook -i $ANSIBLE_INVENTORY $UPDATE_PROXY_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA" + - ansible-playbook -i $ANSIBLE_INVENTORY $MANAGE_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA" rules: - if: $CI_COMMIT_BRANCH =~ /^(demo|stable|staging|test)$/ when: always diff --git a/Dockerfile b/Dockerfile index 54133ab5..99663f2b 100755 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,11 @@ RUN mkdir -p /var/log/metax-api/errors && touch /var/log/metax-api/metax-api.jso RUN apt-get update && apt install xqilla libxerces-c-dev build-essential libssl-dev libffi-dev python-dev libxqilla-dev -y -RUN pip install --upgrade pip wheel poetry -COPY pyproject.toml poetry.lock /code/ +RUN pip install --upgrade pip wheel +COPY requirements.txt /code/ WORKDIR /code -RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi --extras "docs simplexquery" + +RUN pip install -r requirements.txt EXPOSE 8008 EXPOSE 8006 diff --git a/ENV_VARS.md b/ENV_VARS.md index c3fa375c..7afadd56 100755 --- a/ENV_VARS.md +++ b/ENV_VARS.md @@ -14,6 +14,7 @@ copy .env.template to .env and fill the required values from below table. Requir | DATACITE_URL | yes | | | DATACITE_USERNAME | yes | | | DEBUG | no | False | +| DEBUG_TOOLBAR_ENABLED | not | True | Enable debug toolbar on local environment | | DJANGO_ENV | no | local | Specifies the environment, corresponds with the environments found in src/metax_api/settings/environments/ | | DJANGO_SECRET_KEY | yes | | | ELASTIC_SEARCH_HOSTS | no | localhost | Elastic Search instance IPs | diff --git a/README.md b/README.md index 462d285d..b753029d 100755 --- a/README.md +++ b/README.md @@ -16,11 +16,12 @@ The recommended way to run the development setup is to use [Docker-swarm setup]( Install [Poetry](https://python-poetry.org/docs/) for your OS. Navigate to the repository root and run command `poetry install`. this will create and activate new Python virtualenv, installing all necessary Python packages to it. -You can generate traditional requirements.txt file with `poetry export --dev -E simplexquery --without-hashes -f requirements.txt --output requirements.txt` + +You can generate traditional requirements.txt file with `poetry export --dev -E simplexquery -E docs -E swagger --without-hashes -o requirements.txt` ### Managing dependencies -__NOTICE: Please remember to execute `poetry export --dev -E simplexquery --without-hashes -f requirements.txt --output requirements.txt` after any additions, updates or removals.__ +__NOTICE: Please remember to generate the requirements.txt file after any additions, updates or removals using the command above.__ Developer dependencies can be added with command `poetry add -D ` Application dependencies can be added with command `poetry add ` @@ -56,7 +57,11 @@ run the tests with command `DJANGO_ENV=unittests python manage.py test --paralle ### Running coverage (Docker) -`docker exec -it -e DJANGO_ENV=unittests $(docker ps -q -f name="metax-web*") coverage run manage.py test --parallel` +Collect data: `docker exec -it -e DJANGO_ENV=unittests $(docker ps -q -f name="metax-web*") coverage run manage.py test --parallel` + +Combine it: `docker exec -it $(docker ps -q -f name="metax-web*") coverage combine` + +Report: `docker exec -it $(docker ps -q -f name="metax-web*") coverage report` ### Generating coverage report diff --git a/config-swap-stack.yml b/config-swap-stack.yml index 8b4ae404..a89850f7 100644 --- a/config-swap-stack.yml +++ b/config-swap-stack.yml @@ -51,6 +51,17 @@ services: volumes: - metax-rabbitmq:/var/lib/rabbitmq + metax-docs: + image: fairdata-docker.artifactory.ci.csc.fi/fairdata-metax-sphinx + hostname: metax-docs + environment: + DOMAIN: 0.0.0.0:8008 + REMS_ENABLED: "true" + ports: + - 8088:8000 + volumes: + - ./docs/api:/sphinx + volumes: metax-rabbitmq: external: true diff --git a/docker-compose.yml b/docker-compose.yml index 5c52aee5..7e37522f 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,6 +61,17 @@ services: target: /usr/local/apache2/conf/extra/httpd-metax.conf hostname: metax-auth + metax-docs: + image: fairdata-docker.artifactory.ci.csc.fi/fairdata-metax-sphinx + hostname: metax-docs + environment: + DOMAIN: metax.fd-dev.csc.fi + REMS_ENABLED: "true" + ports: + - 8088:8000 + volumes: + - ./docs/api:/sphinx + fairdata-nginx: image: nginx:latest configs: diff --git a/docs/README.md b/docs/README.md deleted file mode 100755 index 938fd972..00000000 --- a/docs/README.md +++ /dev/null @@ -1,11 +0,0 @@ -## Enabling autobuilds - -To install required dependencies run the following: -``pip install sphinx sphinx-autobuild sphinx_rtd_theme`` - -To start server, run following in metax-api directory: -``sphinx-autobuild -t {envtag} docs/source/ docs/build/``, where {envtag} is one of local_development, test, stable or production. Envtag determines the target environment of the documentation. - -Note that the server should be run on the host machine since virtual machine does not build docs automatically. - -To conditionally add parts of the documentation, use only -directive. See [This](https://github.com/sphinx-doc/sphinx/issues/1115) for known issue with this directive and headings. \ No newline at end of file diff --git a/docs/api/README.md b/docs/api/README.md new file mode 100644 index 00000000..ca4615e7 --- /dev/null +++ b/docs/api/README.md @@ -0,0 +1,47 @@ +# Developing documentation + +Metax has API documentation implemented with Swagger and Sphinx. Below are instructions how they can be improved. + +## Swagger + +The openapi/swagger specification version 2 is used to document the Metax REST API. +Rest api descriptions are stored in the repository in /metax_api/swagger/v1/swagger.yaml + and /metax_api/swagger/v2/swagger.yaml, depending on the interface version. +From both yaml files, the corresponding html files are generated in the /metax_api/templates/swagger/ directory when Metax starts. + +Swagger documentation can be edited directly in PyCharm and VS Code. There are good openapi plugins for both. One good plugin for both is the OpenAPI (Swagger) Editor. Another option is to use [Swagger editor](https://editor.swagger.io). + +[VS Code plugin](https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi&ssr=false#review-details) + +[PyCharm plugin](https://plugins.jetbrains.com/plugin/14837-openapi-swagger-editor) + +Although OpenApi is allowed to edit in json and yaml formats, in the case of Metax it has to be done in yaml format because otherwise the conversion to html format will not be possible. +A good starting point for studying OpenApi is [OpenApi specification V2](https://swagger.io/specification/v2/) + +## Sphinx + +The repository provides a Sphinx autobuild server in Docker container for conveniently write and develop the API documentation. +Below are the instructions how to use the server. + +### Building the image + +The autobuild server can be built with following command from repo root: + +`docker build -t fairdata-docker.artifactory.ci.csc.fi/fairdata-metax-sphinx -f sphinx.dockerfile .` + +### Running the server in standalone container + +The server can be run with the following command, also from repo root: + +`docker run -it -v $PWD/docs/api:/sphinx/ -p 8088:8000 fairdata-docker.artifactory.ci.csc.fi/fairdata-metax-sphinx` + +### Running the server in stack + +The autobuild server is also present in both of the stacks provided in the repo. The default dev env domain name `metax.fd-dev.csc.fi`is used in the documentation +so it should be added to `/etc/hosts` file to enable correct redirection in documentation links. When the domain name is added, the server is available from +`http://metax.fd-dev.csc.fi:8088`. By disabling browser cache redirection errors can be prevented. If all other fail, `0.0.0.0:8088` should work. + +### Additional notes + +To conditionally add parts of the documentation, use only -directive. See [This](https://github.com/sphinx-doc/sphinx/issues/1115) for known issue with this +directive and headings. diff --git a/docs/v1/source/_static/custom.css b/docs/api/_static/custom.css similarity index 100% rename from docs/v1/source/_static/custom.css rename to docs/api/_static/custom.css diff --git a/docs/v1/source/conf.py b/docs/api/conf.py similarity index 69% rename from docs/v1/source/conf.py rename to docs/api/conf.py index 13e88985..1b73d209 100755 --- a/docs/v1/source/conf.py +++ b/docs/api/conf.py @@ -12,22 +12,25 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os +import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- -project = 'Metax API' -copyright = '2018, csc.fi' -author = 'csc.fi' +project = "Metax API" +copyright = "2020, csc.fi" +author = "csc.fi" # The short X.Y version -version = '' +version = "" # The full version, including alpha/beta/rc tags -release = '' +release = "" +domain = os.getenv("DOMAIN", "metax.fairdata.fi") +branch = os.getenv("BRANCH", "master") +etsin_url = os.getenv("ETSIN_URL", "etsin.fairdata.fi") # -- General configuration --------------------------------------------------- @@ -38,20 +41,18 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ -] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -66,15 +67,24 @@ exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" +replacements = { + "__METAX_ENV_DOMAIN__": f"{domain}", + "__METAX_ENV_BRANCH__": f"{branch}", + "__ETSIN_ENV_BASE_URL__": f"{etsin_url}" +} + +tags = { + 'rems_enabled': os.getenv("REMS_ENABLED", 'False') +} # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -82,25 +92,25 @@ # # html_theme_options = {} html_theme_options = { - 'canonical_url': '', - 'analytics_id': '', - 'logo_only': False, - 'display_version': True, - 'prev_next_buttons_location': 'bottom', - 'style_external_links': False, + "canonical_url": "", + "analytics_id": "", + "logo_only": False, + "display_version": True, + "prev_next_buttons_location": "bottom", + "style_external_links": False, # Toc options - 'collapse_navigation': True, - 'sticky_navigation': True, - 'navigation_depth': -1, - 'includehidden': True, - 'titles_only': False + "collapse_navigation": True, + "sticky_navigation": True, + "navigation_depth": -1, + "includehidden": True, + "titles_only": False, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -116,7 +126,7 @@ # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'MetaxAPIdoc' +htmlhelp_basename = "MetaxAPIdoc" # -- Options for LaTeX output ------------------------------------------------ @@ -125,15 +135,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -143,8 +150,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'MetaxAPI.tex', 'Metax API Documentation', - 'csc.fi', 'manual'), + (master_doc, "MetaxAPI.tex", "Metax API Documentation", "csc.fi", "manual"), ] @@ -152,10 +158,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'metaxapi', 'Metax API Documentation', - [author], 1) -] +man_pages = [(master_doc, "metaxapi", "Metax API Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -164,10 +167,29 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'MetaxAPI', 'Metax API Documentation', - author, 'MetaxAPI', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "MetaxAPI", + "Metax API Documentation", + author, + "MetaxAPI", + "Part of the Fairdata services.", + "Miscellaneous", + ), ] +# from https://github.com/sphinx-doc/sphinx/issues/4054 +def replace(app, docname, source): + for key, value in app.config.replacements.items(): + source[0] = source[0].replace(key, value) + +def add_tags(app): + for key, value in tags.items(): + if value.lower() in ('true'): + app.tags.add(key) + def setup(app): - app.add_stylesheet('custom.css') + app.add_config_value('replacements', {}, True) + add_tags(app) + app.connect('source-read', replace) + app.add_css_file("custom.css") diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100755 index 00000000..84fcea84 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,26 @@ + +.. Metax API documentation master file, created by + sphinx-quickstart on Wed Sep 5 13:21:50 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +======================= +Metax API Documentation +======================= + +.. toctree:: + :maxdepth: 5 + :caption: Metax API: + + v1/index + v2/index + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/v2/source/_static/custom.css b/docs/api/v1/_static/custom.css similarity index 100% rename from docs/v2/source/_static/custom.css rename to docs/api/v1/_static/custom.css diff --git a/docs/api/v1/_templates/layout.html b/docs/api/v1/_templates/layout.html new file mode 100644 index 00000000..24c77670 --- /dev/null +++ b/docs/api/v1/_templates/layout.html @@ -0,0 +1,7 @@ +{% extends "!layout.html" %} + +{% block extrahead %} + + + +{% endblock %} \ No newline at end of file diff --git a/docs/v1/source/datasets.rst b/docs/api/v1/datasets.rst similarity index 97% rename from docs/v1/source/datasets.rst rename to docs/api/v1/datasets.rst index 6af98008..53115af3 100755 --- a/docs/v1/source/datasets.rst +++ b/docs/api/v1/datasets.rst @@ -20,7 +20,7 @@ Data model visualization The dataset data model visualization can be seen here https://tietomallit.suomi.fi/model/mrd. The data model visualization is very helpful to keep open when creating dataset metadata descriptions, as it shows all the different possible fields and relations, which can be used, which fields are mandatory, and so on. -Additionally, the chosen data catalog may have some additional restrictions to the schema, such as reduced relations, or reduced mandatory fields. Read more about data catalogs and their implications here :ref:`rst-data-catalogs`. +Additionally, the chosen data catalog may have some additional restrictions to the schema, such as reduced relations, or reduced mandatory fields. Read more about data catalogs and their implications here :ref:`rst-data-catalogs-v1`. @@ -74,7 +74,7 @@ Preferred identifier is the "public" identifier of a dataset. When referring to -.. _rst-data-catalogs: +.. _rst-data-catalogs-v1: Data Catalogs ^^^^^^^^^^^^^^ @@ -146,7 +146,7 @@ When creating a new dataset and wishing to use for example the ATT catalog, the assert response.status_code == 201, response.content -For more involving examples, see the :ref:`rst-dataset-examples` section for datasets. +For more involving examples, see the :ref:`examples` section for datasets. @@ -201,7 +201,7 @@ If the requested resource has not been modified after the date specified in the -.. _rst-dataset-versioning: +.. _rst-dataset-versioning-v1: Dataset versioning ------------------- @@ -262,7 +262,7 @@ New metadata versions are not visible in the returned response in any way, excep **How do I know beforehand if a new dataset version is going to be created?** -Take a look at the topic :ref:`rst-describing-and-adding-files`. +Take a look at the topic :ref:`rst-describing-and-adding-files-v1`. @@ -329,12 +329,12 @@ If the field ``alternate_record_set`` is missing from a record, it means there a -.. _rst-describing-and-adding-files: +.. _rst-describing-and-adding-files-v1: Describing files vs. adding and removing files ----------------------------------------------- -A distinction needs to be made between *describing* files in a dataset, and *adding or removing* files. As explained in the topic :ref:`rst-dataset-versioning`, just editing a dataset's metadata (including the dataset-specific file metadata in fields ``research_dataset.files`` and ``research_dataset.directories``) does not produce new dataset versions, while *adding* new files will produce new dataset versions, as will *removing* files. Yet, both describing the files, and adding or removing files, happens by inserting objects inside the fields ``research_dataset.files`` and ``research_dataset.directories``, or by removing the same objects when wishing to remove files from a dataset. How to know which is which, and what to expect when updating datasets and dealing with files? +A distinction needs to be made between *describing* files in a dataset, and *adding or removing* files. As explained in the topic :ref:`rst-dataset-versioning-v1`, just editing a dataset's metadata (including the dataset-specific file metadata in fields ``research_dataset.files`` and ``research_dataset.directories``) does not produce new dataset versions, while *adding* new files will produce new dataset versions, as will *removing* files. Yet, both describing the files, and adding or removing files, happens by inserting objects inside the fields ``research_dataset.files`` and ``research_dataset.directories``, or by removing the same objects when wishing to remove files from a dataset. How to know which is which, and what to expect when updating datasets and dealing with files? **Adding and describing single files** @@ -389,7 +389,7 @@ If you want to use an existing dataset as a template for a new dataset, you can -.. _rst-datasets-reference-data: +.. _rst-datasets-reference-data-v1: Reference data guide --------------------- @@ -448,7 +448,7 @@ All this needs to be taken into account when looking which reference data to use -.. _rst-datasets-reference-data-table: +.. _rst-datasets-reference-data-table-v1: Reference data table ^^^^^^^^^^^^^^^^^^^^^ @@ -463,7 +463,7 @@ It helps to have the `research_dataset data model visualization `. + More information about updating a dataset can be found in :ref:`Update examples`. **Changing license** @@ -584,7 +584,7 @@ It helps to have the `research_dataset data model visualization ` for more information about update process. + Please refer to :ref:`Update examples` for more information about update process. .. note:: Changing the license for REMS managed dataset closes all existing download accesses to the dataset. @@ -601,7 +601,7 @@ It helps to have the `research_dataset data model visualization `` should be provided, in order to retrieve only those files and directories, which are included in the specified dataset. +The second way is by using the same API as is used to generally browse the files of a project (see :ref:`rst-browsing-files-v1`). Browsing the files of a dataset works the same way, except that an additional query parameter ``cr_identifier=`` should be provided, in order to retrieve only those files and directories, which are included in the specified dataset. Example: @@ -1186,14 +1186,14 @@ Using reference data Modifying ``research_dataset`` to contain data that depends on reference data. -Be sure to also check out :ref:`rst-reference-data-query-examples` for useful examples how to browse reference data in general. +Be sure to also check out :ref:`rst-reference-data-query-examples-v1` for useful examples how to browse reference data in general. Add a directory ~~~~~~~~~~~~~~~~ -Below example assumes an existing bare minimum dataset, to which a directory of files is being added. The directory-object has a mandatory field called ``use_category``, which requires using a value from reference data in its ``identifier`` field. In the dataset reference data table on this same page(:ref:`rst-datasets-reference-data-table`), we should be able to find this row: +Below example assumes an existing bare minimum dataset, to which a directory of files is being added. The directory-object has a mandatory field called ``use_category``, which requires using a value from reference data in its ``identifier`` field. In the dataset reference data table on this same page(:ref:`rst-datasets-reference-data-table-v1`), we should be able to find this row: .. code-block:: python diff --git a/docs/v1/source/end_users.rst b/docs/api/v1/end_users.rst similarity index 97% rename from docs/v1/source/end_users.rst rename to docs/api/v1/end_users.rst index 6baeffb0..0fd90f8b 100755 --- a/docs/v1/source/end_users.rst +++ b/docs/api/v1/end_users.rst @@ -11,7 +11,7 @@ Compared to public users, end users have extra access to the following APIs: * ``/rest/directories`` read access for files in user's projects -.. _rst-end-user-authentication: +.. _rst-end-user-authentication-v1: Authentication --------------- diff --git a/docs/v1/source/files.rst b/docs/api/v1/files.rst similarity index 98% rename from docs/v1/source/files.rst rename to docs/api/v1/files.rst index 40c2156b..ee95dca9 100755 --- a/docs/v1/source/files.rst +++ b/docs/api/v1/files.rst @@ -11,7 +11,7 @@ The ``/rest/files`` API supports creating, retrieving, updating, and deleting fi Write-operations to the ``/rest/files`` API is generally limited only to Fairdata services. In practice, new file metadata only appears to Metax as a result of freezing files in the Fairdata IDA service, or during some PAS processes. -End users will only be able to browse file metadata of projects where they are a member, end edit a limited set of metadata fields. Details about browsing files using the Metax API can be found later in this document :ref:`here `, and in swagger. +End users will only be able to browse file metadata of projects where they are a member, end edit a limited set of metadata fields. Details about browsing files using the Metax API can be found later in this document :ref:`here `, and in swagger. @@ -65,7 +65,7 @@ This is just a quick overview, below code examples include some use of them, and In the public browse API's, a dataset's access restrictions or embargoes may apply, and only limited metadata may be returned. Authentication for these public API's is optional, but by authenticating access restrictions may be lifted, for example due to ownership of the published dataset, etc. -.. _rst-files-reference-data: +.. _rst-files-reference-data-v1: Reference data guide --------------------- @@ -130,7 +130,7 @@ Example payload to create a file in Metax (``POST /rest/files``). -.. _rst-browsing-files: +.. _rst-browsing-files-v1: Browsing files ^^^^^^^^^^^^^^^ diff --git a/docs/v1/source/general.rst b/docs/api/v1/general.rst similarity index 99% rename from docs/v1/source/general.rst rename to docs/api/v1/general.rst index 82f81626..527eb980 100755 --- a/docs/v1/source/general.rst +++ b/docs/api/v1/general.rst @@ -55,7 +55,7 @@ An URL of the form https://__METAX_ENV_DOMAIN__/rest/datasets always points to t API Authentication ------------------- -Basic Authentication and Bearer Tokens are used for access control for certain APIs. Basic Authentication credentials are distributed only to known Fairdata services. End Users are able to utilize Bearer tokens in order to interact with certain APIs. Read more about End User authentication and token use at :ref:`rst-end-user-authentication`. +Basic Authentication and Bearer Tokens are used for access control for certain APIs. Basic Authentication credentials are distributed only to known Fairdata services. End Users are able to utilize Bearer tokens in order to interact with certain APIs. Read more about End User authentication and token use at :ref:`rst-end-user-authentication-v1`. Write operations (``POST``, ``PUT``, ``PATCH``, ``DELETE``) always require authentication. Some APIs require no authentication when reading (``GET`` operations), while others do. Authentication-related errors will result in a HTTP 401 or 403 error. diff --git a/docs/v1/source/index.rst b/docs/api/v1/index.rst similarity index 77% rename from docs/v1/source/index.rst rename to docs/api/v1/index.rst index 89205247..9e67db1e 100755 --- a/docs/v1/source/index.rst +++ b/docs/api/v1/index.rst @@ -4,14 +4,14 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -======================= -Metax API Documentation -======================= +========================== +Metax API V1 Documentation +========================== .. toctree:: - :numbered: + :titlesonly: :maxdepth: 5 - :caption: Metax API Contents: + :caption: Contents: metax_api quick_start @@ -21,8 +21,6 @@ Metax API Documentation datasets files reference_data - .. oai_pmh - Indices and tables @@ -31,4 +29,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/v1/source/list_operations.rst b/docs/api/v1/list_operations.rst similarity index 100% rename from docs/v1/source/list_operations.rst rename to docs/api/v1/list_operations.rst diff --git a/docs/api/v1/metax_api.rst b/docs/api/v1/metax_api.rst new file mode 100755 index 00000000..6d10a8f4 --- /dev/null +++ b/docs/api/v1/metax_api.rst @@ -0,0 +1,30 @@ + +Metax API +========== + +**The documentation is work in progress** + +This documentation contains information about how to use the APIs in Metax. If you more or less know what you are doing and want to get your hands dirty right away, head over to :doc:`quick_start`. Otherwise, keep reading. + +Metax is a metadata storage for Finnish research data. The core of the data is based on metadata from other Fairdata services (such as IDA and PAS), but also user-created metadata (by using Qvain, or other means), and metadata harvested from external sources. Read more at https://www.fairdata.fi/en/ and https://www.fairdata.fi/en/metax/. + +Metax does not have a graphical UI. Most of the API's provided by Metax API are accessible only to other Fairdata services, and the main method for interacting with Metax should be by using those services. For advanced end users, Metax API can be accessed directly by using special tokens for authentication. See :doc:`end_users` for more information. + +Any Python code examples in the API documentation are written using Python version 3, and most examples utilize the excellent ``requests`` library. + + +**Metax APIs** + +Below is a rough outline what type of APIs Metax is currently providing. The rest of the documentation will cover how to interact with them. + +.. list-table:: + :header-rows: 1 + + * - API + - Description + * - https://__METAX_ENV_DOMAIN__/rest/v1 + - The main API to interact with most resources inside Metax, such as datasets, files, and data catalogs. + * - https://__METAX_ENV_DOMAIN__/rpc/v1 + - A Remote Procedure Call -API, to execute various actions in Metax, or retrieve data that otherwise does not fit into the REST api. + * - https://__METAX_ENV_DOMAIN__/oai/ + - Implements the OAI-PMH specification. The specification defines a way to harvest filtered sets of data (datasets) in an incremental manner. diff --git a/docs/v1/source/metax_high_level.png b/docs/api/v1/metax_high_level.png similarity index 100% rename from docs/v1/source/metax_high_level.png rename to docs/api/v1/metax_high_level.png diff --git a/docs/v1/source/metax_high_level.xml b/docs/api/v1/metax_high_level.xml similarity index 100% rename from docs/v1/source/metax_high_level.xml rename to docs/api/v1/metax_high_level.xml diff --git a/docs/v1/source/quick_start.rst b/docs/api/v1/quick_start.rst similarity index 100% rename from docs/v1/source/quick_start.rst rename to docs/api/v1/quick_start.rst diff --git a/docs/v2/source/reference_data.rst b/docs/api/v1/reference_data.rst similarity index 60% rename from docs/v2/source/reference_data.rst rename to docs/api/v1/reference_data.rst index 4d4bd526..303c3089 100755 --- a/docs/v2/source/reference_data.rst +++ b/docs/api/v1/reference_data.rst @@ -4,7 +4,7 @@ Reference Data "Reference Data" is a fancy name for "a list of known values". When creating new objects in Metax, or updating existing objects, values for some fields are checked against an approriate category in this pre-defined list of known values. For example, when setting the language of a dataset, "my cat" is not a valid value. Instead, a valid value must be chosen from the language reference data listed at https://__METAX_ENV_DOMAIN__/es/reference_data/language/_search?pretty=true (the link only shows a few values, since the list of languages is pretty huge, but it gives you a glimpse of what reference data in practice looks like). -Some other examples for reference data use include describing the purpose of associated files in a dataset (`Use Category `_), the manner how contributors were involved in a research (`Contributor Role `_), or telling which fields of science the research fits into (`Field of Science `_). The docs for each API has a section dedicated for reference data, which explains when use of reference data is required. For example for datasets, that section can be found here :ref:`rst-datasets-reference-data`. +Some other examples for reference data use include describing the purpose of associated files in a dataset (`Use Category `_), the manner how contributors were involved in a research (`Contributor Role `_), or telling which fields of science the research fits into (`Field of Science `_). The docs for each API has a section dedicated for reference data, which explains when use of reference data is required. For example for datasets, that section can be found here :ref:`rst-datasets-reference-data-v1`. The Metax API documentation contains some examples for querying the reference data index, but since the index is a ElasticSearch database, it is best to refer to the official ElasticSearch documentation for how to build more complex search queries. @@ -15,54 +15,30 @@ Reference Data Indexes Below table contains a full list of the different indexes stored in the reference data. -+----------------------------------------------------------------------------------------+ -| Url to browse reference data | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/access_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/contributor_role/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/contributor_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/event_outcome/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/field_of_science/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/file_format_version/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/file_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/funder_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/identifier_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/keyword/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/language/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/license/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/lifecycle_event/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/location/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/mime_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/preservation_event/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/relation_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/research_infra/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/resource_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/restriction_grounds/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/use_category/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/organization_data/organization/_search?pretty=true | -+----------------------------------------------------------------------------------------+ - +.. list-table:: + + * - https://__METAX_ENV_DOMAIN__/es/reference_data/access_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/contributor_role/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/contributor_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/event_outcome/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/field_of_science/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/file_format_version/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/file_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/funder_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/identifier_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/keyword/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/language/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/license/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/lifecycle_event/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/location/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/mime_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/preservation_event/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/relation_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/research_infra/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/resource_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/restriction_grounds/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/use_category/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/organization_data/organization/_search?pretty=true How to use reference data when uploading data to Metax? @@ -151,7 +127,7 @@ Most often, the field that will get populated from the reference data will be th -.. _rst-reference-data-query-examples: +.. _rst-reference-data-query-examples-v1: Querying Reference Data ------------------------ diff --git a/docs/api/v2/_templates/layout.html b/docs/api/v2/_templates/layout.html new file mode 100644 index 00000000..24c77670 --- /dev/null +++ b/docs/api/v2/_templates/layout.html @@ -0,0 +1,7 @@ +{% extends "!layout.html" %} + +{% block extrahead %} + + + +{% endblock %} \ No newline at end of file diff --git a/docs/v2/source/datasets.rst b/docs/api/v2/datasets.rst similarity index 98% rename from docs/v2/source/datasets.rst rename to docs/api/v2/datasets.rst index ab928343..5281c8ff 100755 --- a/docs/v2/source/datasets.rst +++ b/docs/api/v2/datasets.rst @@ -22,7 +22,7 @@ Data model visualization The dataset data model visualization can be seen here https://tietomallit.suomi.fi/model/mrd. The data model visualization is very helpful to keep open when creating dataset metadata descriptions, as it shows all the different possible fields and relations, which can be used, which fields are mandatory, and so on. -Additionally, the chosen data catalog may have some additional restrictions to the schema, such as reduced relations, or reduced mandatory fields. Read more about data catalogs and their implications here :ref:`rst-data-catalogs`. +Additionally, the chosen data catalog may have some additional restrictions to the schema, such as reduced relations, or reduced mandatory fields. Read more about data catalogs and their implications here :ref:`rst-data-catalogs-v2`. @@ -80,7 +80,7 @@ Preferred identifier is the "public" identifier of a dataset. When referring to When a user has added some files to a dataset, the user can choose to write additional descriptions to those files. The files already include various automatically generated technical metadata, such as byte sizes, mime types, checksum values and algorithms and such, but any extra metadata that the user wishes to enter about some file is called "user metadata". -.. _rst-data-catalogs: +.. _rst-data-catalogs-v2: Data Catalogs ^^^^^^^^^^^^^^ @@ -160,7 +160,7 @@ When creating a new dataset and wishing to use for example the ATT catalog, the assert response.status_code == 201, response.content -For more involving examples, see the :ref:`rst-dataset-examples` section for datasets. +For more involving examples, see the :ref:`examples` section for datasets. @@ -174,7 +174,7 @@ Dataset lifecycle in Metax * The dataset can be premanently deleted at any time by the user. 2) A dataset is published. When the dataset is published: * The dataset's metadata descriptions can still be updated at any time. - * Files can no longer be freely added or removed (a couple of exceptions remain, see :ref:`rst-dataset-versioning`). + * Files can no longer be freely added or removed (a couple of exceptions remain, see :ref:`rst-dataset-versioning-v2`). * The dataset becomes publicly findable (any selected access restrictions, such as embargo, applies). * Dataset receives permanent resolvable identifiers. * Dataset can no longer be premanently deleted. A tombstone page will remain after deletion. @@ -231,7 +231,7 @@ If the requested resource has not been modified after the date specified in the -.. _rst-dataset-versioning: +.. _rst-dataset-versioning-v2: Dataset versioning ------------------- @@ -360,7 +360,7 @@ If you want to use an existing dataset as a template for a new dataset, you can -.. _rst-datasets-reference-data: +.. _rst-datasets-reference-data-v2: Reference data guide --------------------- @@ -419,7 +419,7 @@ All this needs to be taken into account when looking which reference data to use -.. _rst-datasets-reference-data-table: +.. _rst-datasets-reference-data-table-v2: Reference data table ^^^^^^^^^^^^^^^^^^^^^ @@ -434,7 +434,7 @@ It helps to have the `research_dataset data model visualization `. + More information about updating a dataset can be found in :ref:`Update examples`. **Changing license** @@ -555,7 +555,7 @@ It helps to have the `research_dataset data model visualization ` for more information about update process. + Please refer to :ref:`Update examples` for more information about update process. .. note:: Changing the license for REMS managed dataset closes all existing download accesses to the dataset. @@ -572,7 +572,7 @@ It helps to have the `research_dataset data model visualization `` should be provided, in order to retrieve only those files and directories, which are included in the specified dataset. +The second way is by using the same API as is used to generally browse the files of a project (see :ref:`rst-browsing-files-v2`). Browsing the files of a dataset works the same way, except that an additional query parameter ``cr_identifier=`` should be provided, in order to retrieve only those files and directories, which are included in the specified dataset. Example: @@ -1446,14 +1446,14 @@ Using reference data Modifying ``research_dataset`` to contain data that depends on reference data. -Be sure to also check out :ref:`rst-reference-data-query-examples` for useful examples how to browse reference data in general. +Be sure to also check out :ref:`rst-reference-data-query-examples-v2` for useful examples how to browse reference data in general. Add a directory ~~~~~~~~~~~~~~~~ -Below example assumes an existing bare minimum draft dataset, to which some files have already been added. This example adds some user metadata to that directory. The directory-object has a mandatory field called ``use_category``, which requires using a value from reference data in its ``identifier`` field. In the dataset reference data table on this same page(:ref:`rst-datasets-reference-data-table`), we should be able to find this row: +Below example assumes an existing bare minimum draft dataset, to which some files have already been added. This example adds some user metadata to that directory. The directory-object has a mandatory field called ``use_category``, which requires using a value from reference data in its ``identifier`` field. In the dataset reference data table on this same page(:ref:`rst-datasets-reference-data-table-v2`), we should be able to find this row: .. code-block:: python diff --git a/docs/v2/source/end_users.rst b/docs/api/v2/end_users.rst similarity index 97% rename from docs/v2/source/end_users.rst rename to docs/api/v2/end_users.rst index 0b766920..37801e63 100755 --- a/docs/v2/source/end_users.rst +++ b/docs/api/v2/end_users.rst @@ -12,7 +12,7 @@ Compared to public users, end users have extra access to the following APIs: * ``/rpc/v2/datasets`` write access to owned datasets -.. _rst-end-user-authentication: +.. _rst-end-user-authentication-v2: Authentication --------------- diff --git a/docs/v2/source/files.rst b/docs/api/v2/files.rst similarity index 98% rename from docs/v2/source/files.rst rename to docs/api/v2/files.rst index db8500f8..024a52e9 100755 --- a/docs/v2/source/files.rst +++ b/docs/api/v2/files.rst @@ -11,7 +11,7 @@ The ``/rest/files`` API supports creating, retrieving, updating, and deleting fi Write-operations to the ``/rest/files`` API is generally limited only to Fairdata services. In practice, new file metadata only appears to Metax as a result of freezing files in the Fairdata IDA service, or during some PAS processes. -End users will only be able to browse file metadata of projects where they are a member, end edit a limited set of metadata fields. Details about browsing files using the Metax API can be found later in this document :ref:`here `, and in swagger. +End users will only be able to browse file metadata of projects where they are a member, end edit a limited set of metadata fields. Details about browsing files using the Metax API can be found later in this document :ref:`here `, and in swagger. @@ -65,7 +65,7 @@ This is just a quick overview, below code examples include some use of them, and In the public browse API's, a dataset's access restrictions or embargoes may apply, and only limited metadata may be returned. Authentication for these public API's is optional, but by authenticating access restrictions may be lifted, for example due to ownership of the published dataset, etc. -.. _rst-files-reference-data: +.. _rst-files-reference-data-v2: Reference data guide --------------------- @@ -129,7 +129,7 @@ Example payload to create a file in Metax (``POST /rest/files``). -.. _rst-browsing-files: +.. _rst-browsing-files-v2: Browsing files ^^^^^^^^^^^^^^^ diff --git a/docs/v2/source/general.rst b/docs/api/v2/general.rst similarity index 99% rename from docs/v2/source/general.rst rename to docs/api/v2/general.rst index 4797e00b..3f82b5d9 100755 --- a/docs/v2/source/general.rst +++ b/docs/api/v2/general.rst @@ -60,7 +60,7 @@ An URL of the form https://__METAX_ENV_DOMAIN__/rest/datasets (no API version sp API Authentication ------------------- -Basic Authentication and Bearer Tokens are used for access control for certain APIs. Basic Authentication credentials are distributed only to known Fairdata services. End Users are able to utilize Bearer tokens in order to interact with certain APIs. Read more about End User authentication and token use at :ref:`rst-end-user-authentication`. +Basic Authentication and Bearer Tokens are used for access control for certain APIs. Basic Authentication credentials are distributed only to known Fairdata services. End Users are able to utilize Bearer tokens in order to interact with certain APIs. Read more about End User authentication and token use at :ref:`rst-end-user-authentication-v2`. Write operations (``POST``, ``PUT``, ``PATCH``, ``DELETE``) always require authentication. Some APIs require no authentication when reading (``GET`` operations), while others do. Authentication-related errors will result in a HTTP 401 or 403 error. diff --git a/docs/v2/source/index.rst b/docs/api/v2/index.rst similarity index 77% rename from docs/v2/source/index.rst rename to docs/api/v2/index.rst index 89205247..f3cc2183 100755 --- a/docs/v2/source/index.rst +++ b/docs/api/v2/index.rst @@ -4,14 +4,14 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -======================= -Metax API Documentation -======================= +========================== +Metax API V2 Documentation +========================== .. toctree:: - :numbered: + :titlesonly: :maxdepth: 5 - :caption: Metax API Contents: + :caption: Contents: metax_api quick_start @@ -21,8 +21,6 @@ Metax API Documentation datasets files reference_data - .. oai_pmh - Indices and tables @@ -31,4 +29,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/v2/source/list_operations.rst b/docs/api/v2/list_operations.rst similarity index 100% rename from docs/v2/source/list_operations.rst rename to docs/api/v2/list_operations.rst diff --git a/docs/api/v2/metax_api.rst b/docs/api/v2/metax_api.rst new file mode 100755 index 00000000..0a1aa86c --- /dev/null +++ b/docs/api/v2/metax_api.rst @@ -0,0 +1,29 @@ + +Metax API +========== + +This documentation contains information about how to use the APIs in Metax. If you more or less know what you are doing and want to get your hands dirty right away, head over to :doc:`quick_start`. Otherwise, keep reading. + +Metax is a metadata storage for Finnish research data. The core of the data is based on metadata from other Fairdata services (such as IDA and PAS), but also user-created metadata (by using Qvain, or other means), and metadata harvested from external sources. Read more at https://www.fairdata.fi/en/ and https://www.fairdata.fi/en/metax/. + +Metax does not have a graphical UI. Most of the API's provided by Metax API are accessible only to other Fairdata services, and the main method for interacting with Metax should be by using those services. For advanced end users, Metax API can be accessed directly by using special tokens for authentication. See :doc:`end_users` for more information. + +Any Python code examples in the API documentation are written using Python version 3, and most examples utilize the excellent ``requests`` library. + + +**Metax APIs** + +Below is a rough outline what type of APIs Metax is currently providing. The rest of the documentation will cover how to interact with them. + +.. list-table:: + :widths: 50 150 + :header-rows: 1 + + * - API + - Description + * - https://__METAX_ENV_DOMAIN__/rest/v2 + - The main API to interact with most resources inside Metax, such as datasets, files, and data catalogs. + * - https://__METAX_ENV_DOMAIN__/rpc/v2 + - A Remote Procedure Call -API, to execute various actions in Metax, or retrieve data that otherwise does not fit into the REST api. + * - https://__METAX_ENV_DOMAIN__/oai/ + - Implements the OAI-PMH specification. The specification defines a way to harvest filtered sets of data (datasets) in an incremental manner. diff --git a/docs/v2/source/metax_high_level.png b/docs/api/v2/metax_high_level.png similarity index 100% rename from docs/v2/source/metax_high_level.png rename to docs/api/v2/metax_high_level.png diff --git a/docs/v2/source/metax_high_level.xml b/docs/api/v2/metax_high_level.xml similarity index 100% rename from docs/v2/source/metax_high_level.xml rename to docs/api/v2/metax_high_level.xml diff --git a/docs/v2/source/quick_start.rst b/docs/api/v2/quick_start.rst similarity index 100% rename from docs/v2/source/quick_start.rst rename to docs/api/v2/quick_start.rst diff --git a/docs/v1/source/reference_data.rst b/docs/api/v2/reference_data.rst similarity index 60% rename from docs/v1/source/reference_data.rst rename to docs/api/v2/reference_data.rst index 4d4bd526..08b0da86 100755 --- a/docs/v1/source/reference_data.rst +++ b/docs/api/v2/reference_data.rst @@ -4,7 +4,7 @@ Reference Data "Reference Data" is a fancy name for "a list of known values". When creating new objects in Metax, or updating existing objects, values for some fields are checked against an approriate category in this pre-defined list of known values. For example, when setting the language of a dataset, "my cat" is not a valid value. Instead, a valid value must be chosen from the language reference data listed at https://__METAX_ENV_DOMAIN__/es/reference_data/language/_search?pretty=true (the link only shows a few values, since the list of languages is pretty huge, but it gives you a glimpse of what reference data in practice looks like). -Some other examples for reference data use include describing the purpose of associated files in a dataset (`Use Category `_), the manner how contributors were involved in a research (`Contributor Role `_), or telling which fields of science the research fits into (`Field of Science `_). The docs for each API has a section dedicated for reference data, which explains when use of reference data is required. For example for datasets, that section can be found here :ref:`rst-datasets-reference-data`. +Some other examples for reference data use include describing the purpose of associated files in a dataset (`Use Category `_), the manner how contributors were involved in a research (`Contributor Role `_), or telling which fields of science the research fits into (`Field of Science `_). The docs for each API has a section dedicated for reference data, which explains when use of reference data is required. For example for datasets, that section can be found here :ref:`rst-datasets-reference-data-v2`. The Metax API documentation contains some examples for querying the reference data index, but since the index is a ElasticSearch database, it is best to refer to the official ElasticSearch documentation for how to build more complex search queries. @@ -15,53 +15,30 @@ Reference Data Indexes Below table contains a full list of the different indexes stored in the reference data. -+----------------------------------------------------------------------------------------+ -| Url to browse reference data | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/access_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/contributor_role/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/contributor_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/event_outcome/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/field_of_science/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/file_format_version/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/file_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/funder_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/identifier_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/keyword/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/language/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/license/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/lifecycle_event/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/location/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/mime_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/preservation_event/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/relation_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/research_infra/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/resource_type/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/restriction_grounds/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/reference_data/use_category/_search?pretty=true | -+----------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/es/organization_data/organization/_search?pretty=true | -+----------------------------------------------------------------------------------------+ +.. list-table:: + + * - https://__METAX_ENV_DOMAIN__/es/reference_data/access_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/contributor_role/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/contributor_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/event_outcome/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/field_of_science/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/file_format_version/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/file_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/funder_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/identifier_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/keyword/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/language/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/license/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/lifecycle_event/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/location/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/mime_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/preservation_event/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/relation_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/research_infra/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/resource_type/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/restriction_grounds/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/reference_data/use_category/_search?pretty=true + * - https://__METAX_ENV_DOMAIN__/es/organization_data/organization/_search?pretty=true @@ -151,7 +128,7 @@ Most often, the field that will get populated from the reference data will be th -.. _rst-reference-data-query-examples: +.. _rst-reference-data-query-examples-v2: Querying Reference Data ------------------------ diff --git a/docs/docker-stack.md b/docs/docker-stack.md index e39a8926..7c914e9d 100644 --- a/docs/docker-stack.md +++ b/docs/docker-stack.md @@ -48,29 +48,35 @@ To change existing configuration in metax-web container: ## Building related images -Docker image for metax-web can be built with the following command: +Docker images needed in the development can be built with the following commands: `docker build -t fairdata-docker.artifactory.ci.csc.fi/fairdata-metax-web .` -Image for metax-httpd can be build with: - `docker build -t fairdata-docker.artifactory.ci.csc.fi/fairdata-metax-httpd -f httpd.dockerfile .` +`docker build -t fairdata-docker.artifactory.ci.csc.fi/fairdata-metax-sphinx -f sphinx.dockerfile .` + ## Pushing images to Artifactory Ensure that you are logged in to Artifactory: `docker login fairdata-docker.artifactory.ci.csc.fi` -Push commands for metax-web and metax-httpd respectively: +Push commands for docker images: `docker push fairdata-docker.artifactory.ci.csc.fi/fairdata-metax-web` `docker push fairdata-docker.artifactory.ci.csc.fi/fairdata-metax-httpd` +`docker push fairdata-docker.artifactory.ci.csc.fi/fairdata-metax-sphinx` + ## Running Metax management commands To run Metax management commands, locate the running metax-dev_metax-web container (can be done with `docker container ls`) and open terminal inside it with: `docker exec -it bash` + +## Developing API documentation + +The stack also contains a Sphinx autobuild server for documentation development. More specific instructions can be found from [here](api/README.md). diff --git a/docs/v1/source/metax_api.rst b/docs/v1/source/metax_api.rst deleted file mode 100755 index aa9585ca..00000000 --- a/docs/v1/source/metax_api.rst +++ /dev/null @@ -1,28 +0,0 @@ - -Metax API -========== - -**The documentation is work in progress** - -This documentation contains information about how to use the APIs in Metax. If you more or less know what you are doing and want to get your hands dirty right away, head over to :doc:`quick_start`. Otherwise, keep reading. - -Metax is a metadata storage for Finnish research data. The core of the data is based on metadata from other Fairdata services (such as IDA and PAS), but also user-created metadata (by using Qvain, or other means), and metadata harvested from external sources. Read more at https://www.fairdata.fi/en/ and https://www.fairdata.fi/en/metax/. - -Metax does not have a graphical UI. Most of the API's provided by Metax API are accessible only to other Fairdata services, and the main method for interacting with Metax should be by using those services. For advanced end users, Metax API can be accessed directly by using special tokens for authentication. See :doc:`end_users` for more information. - -Any Python code examples in the API documentation are written using Python version 3, and most examples utilize the excellent ``requests`` library. - - -**Metax APIs** - -Below is a rough outline what type of APIs Metax is currently providing. The rest of the documentation will cover how to interact with them. - -+-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+ -| API | Description | -+-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/rest | The main API to interact with most resources inside Metax, such as datasets, files, and data catalogs. | -+-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/rpc | A Remote Procedure Call -API, to execute various actions in Metax, or retrieve data that otherwise does not fit into the REST api. | -+-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/oaipmh | Implements the OAI-PMH specification. The specification defines a way to harvest filtered sets of data (datasets) in an incremental manner. | -+-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/docs/v1/source/oai_pmh.rst b/docs/v1/source/oai_pmh.rst deleted file mode 100755 index 239efefd..00000000 --- a/docs/v1/source/oai_pmh.rst +++ /dev/null @@ -1,5 +0,0 @@ - -OAI-PMH -======== - -This thing also exists. diff --git a/docs/v2/source/conf.py b/docs/v2/source/conf.py deleted file mode 100755 index 15550993..00000000 --- a/docs/v2/source/conf.py +++ /dev/null @@ -1,173 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/master/config - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = 'Metax API' -copyright = '2020, csc.fi' -author = 'csc.fi' - -# The short X.Y version -version = '' -# The full version, including alpha/beta/rc tags -release = '' - - -# -- General configuration --------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path . -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} -html_theme_options = { - 'canonical_url': '', - 'analytics_id': '', - 'logo_only': False, - 'display_version': True, - 'prev_next_buttons_location': 'bottom', - 'style_external_links': False, - # Toc options - 'collapse_navigation': True, - 'sticky_navigation': True, - 'navigation_depth': -1, - 'includehidden': True, - 'titles_only': False -} - - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don't match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', -# 'searchbox.html']``. -# -# html_sidebars = {} - - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = 'MetaxAPIdoc' - - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'MetaxAPI.tex', 'Metax API Documentation', - 'csc.fi', 'manual'), -] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'metaxapi', 'Metax API Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'MetaxAPI', 'Metax API Documentation', - author, 'MetaxAPI', 'Part of the Fairdata services.', - 'Miscellaneous'), -] - -def setup(app): - app.add_stylesheet('custom.css') diff --git a/docs/v2/source/metax_api.rst b/docs/v2/source/metax_api.rst deleted file mode 100755 index d4cab41a..00000000 --- a/docs/v2/source/metax_api.rst +++ /dev/null @@ -1,26 +0,0 @@ - -Metax API -========== - -This documentation contains information about how to use the APIs in Metax. If you more or less know what you are doing and want to get your hands dirty right away, head over to :doc:`quick_start`. Otherwise, keep reading. - -Metax is a metadata storage for Finnish research data. The core of the data is based on metadata from other Fairdata services (such as IDA and PAS), but also user-created metadata (by using Qvain, or other means), and metadata harvested from external sources. Read more at https://www.fairdata.fi/en/ and https://www.fairdata.fi/en/metax/. - -Metax does not have a graphical UI. Most of the API's provided by Metax API are accessible only to other Fairdata services, and the main method for interacting with Metax should be by using those services. For advanced end users, Metax API can be accessed directly by using special tokens for authentication. See :doc:`end_users` for more information. - -Any Python code examples in the API documentation are written using Python version 3, and most examples utilize the excellent ``requests`` library. - - -**Metax APIs** - -Below is a rough outline what type of APIs Metax is currently providing. The rest of the documentation will cover how to interact with them. - -+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+ -| API | Description | -+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/rest/v2 | The main API to interact with most resources inside Metax, such as datasets, files, and data catalogs. | -+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/rpc/v2 | A Remote Procedure Call -API, to execute various actions in Metax, or retrieve data that otherwise does not fit into the REST api. | -+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+ -| https://__METAX_ENV_DOMAIN__/oaipmh | Implements the OAI-PMH specification. The specification defines a way to harvest filtered sets of data (datasets) in an incremental manner. | -+--------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/docs/v2/source/oai_pmh.rst b/docs/v2/source/oai_pmh.rst deleted file mode 100755 index 239efefd..00000000 --- a/docs/v2/source/oai_pmh.rst +++ /dev/null @@ -1,5 +0,0 @@ - -OAI-PMH -======== - -This thing also exists. diff --git a/poetry.lock b/poetry.lock index 390f7345..05e4a555 100644 --- a/poetry.lock +++ b/poetry.lock @@ -24,7 +24,7 @@ python-versions = "*" [[package]] name = "asgiref" -version = "3.3.4" +version = "3.4.1" description = "ASGI specs, helper code, and adapters" category = "main" optional = false @@ -52,17 +52,17 @@ test = ["astroid", "pytest"] [[package]] name = "attrs" -version = "20.3.0" +version = "21.2.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] -docs = ["furo", "sphinx", "zope.interface"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] name = "autosemver" @@ -118,7 +118,7 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "certifi" -version = "2020.12.5" +version = "2021.5.30" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -134,11 +134,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "click" -version = "7.1.2" +version = "8.0.1" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" @@ -164,7 +168,7 @@ toml = ["toml"] [[package]] name = "datacite" -version = "1.1.1" +version = "1.1.2" description = "Python API wrapper for the DataCite Metadata Store API." category = "main" optional = false @@ -183,7 +187,7 @@ tests = ["responses (>=0.10.6)", "mock (>=1.3.0)", "pytest-invenio (>=1.4.0)"] [[package]] name = "decorator" -version = "5.0.7" +version = "5.0.9" description = "Decorators for Humans" category = "dev" optional = false @@ -191,7 +195,7 @@ python-versions = ">=3.5" [[package]] name = "django" -version = "3.1.8" +version = "3.1.13" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -294,7 +298,7 @@ https = ["urllib3[secure] (>=1.24.1)"] [[package]] name = "elasticsearch" -version = "7.12.1" +version = "7.13.3" description = "Python client for Elasticsearch" category = "main" optional = false @@ -312,7 +316,7 @@ requests = ["requests (>=2.4.0,<3.0.0)"] [[package]] name = "executing" -version = "0.6.0" +version = "0.7.0" description = "Get the currently executing AST node of a frame, and other information" category = "dev" optional = false @@ -334,7 +338,7 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "icecream" -version = "2.1.0" +version = "2.1.1" description = "Never use print() to debug again; inspect variables, expressions, and program execution with a single, simple function call." category = "dev" optional = false @@ -381,7 +385,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.0.1" +version = "4.6.1" description = "Read metadata from Python packages" category = "main" optional = false @@ -393,23 +397,25 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "ipdb" -version = "0.13.7" +version = "0.13.9" description = "IPython-enabled pdb" category = "dev" optional = false python-versions = ">=2.7" [package.dependencies] +decorator = {version = "*", markers = "python_version > \"3.6\""} ipython = {version = ">=7.17.0", markers = "python_version > \"3.6\""} toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} [[package]] name = "ipython" -version = "7.22.0" +version = "7.25.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false @@ -421,6 +427,7 @@ backcall = "*" colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" jedi = ">=0.16" +matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} pickleshare = "*" prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" @@ -428,7 +435,7 @@ pygments = "*" traitlets = ">=4.2" [package.extras] -all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.16)", "pygments", "qtconsole", "requests", "testpath"] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] doc = ["Sphinx (>=1.3)"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] @@ -436,7 +443,7 @@ nbformat = ["nbformat"] notebook = ["notebook", "ipywidgets"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.16)"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] [[package]] name = "ipython-genutils" @@ -473,16 +480,17 @@ six = "*" [[package]] name = "isort" -version = "5.8.0" +version = "5.9.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6.1,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] [[package]] name = "jedi" @@ -501,17 +509,17 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] [[package]] name = "jinja2" -version = "2.11.3" +version = "3.0.1" description = "A very fast and expressive template engine." category = "main" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -MarkupSafe = ">=0.23" +MarkupSafe = ">=2.0" [package.extras] -i18n = ["Babel (>=0.8)"] +i18n = ["Babel (>=2.7)"] [[package]] name = "jsonschema" @@ -559,11 +567,22 @@ source = ["Cython (>=0.29.7)"] [[package]] name = "markupsafe" -version = "1.1.1" +version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = true -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" + +[[package]] +name = "matplotlib-inline" +version = "0.1.2" +description = "Inline Matplotlib backend for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +traitlets = "*" [[package]] name = "mypy-extensions" @@ -575,11 +594,11 @@ python-versions = "*" [[package]] name = "packaging" -version = "20.9" +version = "21.0" description = "Core utilities for Python packages" category = "main" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2" @@ -638,7 +657,7 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.18" +version = "3.0.19" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false @@ -649,11 +668,11 @@ wcwidth = "*" [[package]] name = "psycopg2-binary" -version = "2.8.6" +version = "2.9.1" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" [[package]] name = "ptyprocess" @@ -665,7 +684,7 @@ python-versions = "*" [[package]] name = "pygments" -version = "2.8.1" +version = "2.9.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false @@ -687,16 +706,23 @@ tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] [[package]] name = "pyoai" -version = "2.5.0" -description = "The oaipmh module is a Python implementation of an \"Open Archives\nInitiative Protocol for Metadata Harvesting\" (version 2) client and server.\nThe protocol is described here:\n\nhttp://www.openarchives.org/OAI/openarchivesprotocol.html" +version = "2.5.1(unreleased)" +description = "" category = "main" optional = false python-versions = "*" +develop = false [package.dependencies] lxml = "*" six = "*" +[package.source] +type = "git" +url = "https://github.com/infrae/pyoai" +reference = "5f6eba12" +resolved_reference = "5f6eba1201270d930ed684e15e9b9fc885649d17" + [[package]] name = "pyparsing" version = "2.4.7" @@ -707,11 +733,11 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pyrsistent" -version = "0.17.3" +version = "0.18.0" description = "Persistent/Functional/Immutable data structures" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [[package]] name = "python-box" @@ -755,6 +781,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pyyaml" +version = "5.4.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + [[package]] name = "rdflib" version = "5.0.0" @@ -787,7 +821,7 @@ hiredis = ["hiredis (>=0.1.3)"] [[package]] name = "regex" -version = "2021.4.4" +version = "2021.7.6" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -829,7 +863,7 @@ tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake [[package]] name = "six" -version = "1.15.0" +version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "main" optional = false @@ -845,17 +879,17 @@ python-versions = "*" [[package]] name = "sphinx" -version = "3.5.4" +version = "4.0.3" description = "Python documentation generator" category = "main" optional = true -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.12,<0.17" +docutils = ">=0.14,<0.18" imagesize = "*" Jinja2 = ">=2.3" packaging = "*" @@ -871,7 +905,7 @@ sphinxcontrib-serializinghtml = "*" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.800)", "docutils-stubs"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.900)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] @@ -931,11 +965,11 @@ test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "1.0.3" +version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" category = "main" optional = true -python-versions = ">=3.5" +python-versions = ">=3.6" [package.extras] lint = ["flake8", "mypy", "docutils-stubs"] @@ -966,7 +1000,7 @@ test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.4" +version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." category = "main" optional = true @@ -1048,7 +1082,7 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "3.7.4.3" +version = "3.10.0.0" description = "Backported and Experimental Type Hints for Python 3.5+" category = "main" optional = false @@ -1056,16 +1090,16 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.4" +version = "1.26.6" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] +brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -brotli = ["brotlipy (>=0.6.0)"] [[package]] name = "wcwidth" @@ -1085,7 +1119,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "zipp" -version = "3.4.1" +version = "3.5.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1093,16 +1127,17 @@ python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [extras] docs = ["Sphinx", "sphinx-autobuild", "sphinx-rtd-theme"] simplexquery = ["python-simplexquery"] +swagger = ["PyYAML"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "8fba4a295a68a34d35e8f3da03922740c3fdfbe84edf00945c7c67d0016b6115" +content-hash = "f902a1619b9a3a0cdc7233bee55b16b6b3167d7ce087716503df3c9bd39fe484" [metadata.files] alabaster = [ @@ -1118,16 +1153,16 @@ appnope = [ {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, ] asgiref = [ - {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, - {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, + {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, ] asttokens = [ {file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"}, {file = "asttokens-2.0.5.tar.gz", hash = "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5"}, ] attrs = [ - {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, - {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] autosemver = [ {file = "autosemver-0.5.5.tar.gz", hash = "sha256:0af1e8a9c3604545c067311f1c26403e8f0d60b5d9561c0217e14eee21c98b02"}, @@ -1144,16 +1179,16 @@ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] certifi = [ - {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, - {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, + {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, + {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, ] chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1214,16 +1249,16 @@ coverage = [ {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] datacite = [ - {file = "datacite-1.1.1-py2.py3-none-any.whl", hash = "sha256:842b52b47380b658c728f4596dcab4fdaf524ebcfb895a337be267b4a2e9b1bf"}, - {file = "datacite-1.1.1.tar.gz", hash = "sha256:4e3d3153d849f0a5f331ba585bf4d2f6fcc87f63738bbf9621f19f3727ae3e4d"}, + {file = "datacite-1.1.2-py2.py3-none-any.whl", hash = "sha256:a9dc41df3413e2d7d854bda5cea38cfdccabc9cd6ce3652cecd47db534c76c29"}, + {file = "datacite-1.1.2.tar.gz", hash = "sha256:0164bc2ff35bba643897201eb359611abb43ff5811a9ac17fb5592cd643b4443"}, ] decorator = [ - {file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"}, - {file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"}, + {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, + {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, ] django = [ - {file = "Django-3.1.8-py3-none-any.whl", hash = "sha256:c348b3ddc452bf4b62361f0752f71a339140c777ebea3cdaaaa8fdb7f417a862"}, - {file = "Django-3.1.8.tar.gz", hash = "sha256:f8393103e15ec2d2d313ccbb95a3f1da092f9f58d74ac1c61ca2ac0436ae1eac"}, + {file = "Django-3.1.13-py3-none-any.whl", hash = "sha256:a6e0d1ff11095b7394c079ade7094c73b2dc3df4a7a373c9b58ed73b77a97feb"}, + {file = "Django-3.1.13.tar.gz", hash = "sha256:9f8be75646f62204320b195062b1d696ba28aa3d45ee72fb7c888ffaebc5bdb2"}, ] django-debug-toolbar = [ {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"}, @@ -1262,19 +1297,19 @@ dulwich = [ {file = "dulwich-0.19.16.tar.gz", hash = "sha256:f74561c448bfb6f04c07de731c1181ae4280017f759b0bb04fa5770aa84ca850"}, ] elasticsearch = [ - {file = "elasticsearch-7.12.1-py2.py3-none-any.whl", hash = "sha256:1840fea8c305224b8c28acabc8697f739cdfb03618f2d2427b42838971a787f6"}, - {file = "elasticsearch-7.12.1.tar.gz", hash = "sha256:df35d8c638f946f098a74681b18611bdf27ba469fa2063e3dfc8bdc290b11419"}, + {file = "elasticsearch-7.13.3-py2.py3-none-any.whl", hash = "sha256:76cc7670449676138acbab8872eb34867db033701310d9b01fb2ecfba5fb7234"}, + {file = "elasticsearch-7.13.3.tar.gz", hash = "sha256:d539d82552804b3d41033377b9adf11c39c749ee1764af3d74b56f42177e3281"}, ] executing = [ - {file = "executing-0.6.0-py2.py3-none-any.whl", hash = "sha256:a2f10f802b4312b92bd256279b43720271b0d9b540a0dbab7be4c28fbc536479"}, - {file = "executing-0.6.0.tar.gz", hash = "sha256:a07046e608c56948a899e1c7dc45327ed84ee67edf245041eb8c6722658c14e3"}, + {file = "executing-0.7.0-py2.py3-none-any.whl", hash = "sha256:1971c98963857f2c03f4b688d93fc4b28ce756bd102955ea8ea7ce0a7fd9a28f"}, + {file = "executing-0.7.0.tar.gz", hash = "sha256:509fe590e9da1c0659a273c42493a25af6f43d61cf36f085fc1b6cf2c6419d1f"}, ] gunicorn = [ {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] icecream = [ - {file = "icecream-2.1.0-py2.py3-none-any.whl", hash = "sha256:4c441862751e9a8b52c1a5b551056a3430361230b8c845a7953f8b6e400a4f27"}, - {file = "icecream-2.1.0.tar.gz", hash = "sha256:c2e7b74c1c12caa2cfde050f2e636493ee77a9fb4a494b5593418ab359924a24"}, + {file = "icecream-2.1.1-py2.py3-none-any.whl", hash = "sha256:adc1c48f5a4f83b2171c774a35142f217d317a84fca8cdf3fc65aa1321ff26b6"}, + {file = "icecream-2.1.1.tar.gz", hash = "sha256:47e00e3f4e8477996e7dc420b6fa8ba53f8ced17de65320fedb5b15997b76589"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, @@ -1289,15 +1324,15 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.0.1-py3-none-any.whl", hash = "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"}, - {file = "importlib_metadata-4.0.1.tar.gz", hash = "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581"}, + {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, + {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, ] ipdb = [ - {file = "ipdb-0.13.7.tar.gz", hash = "sha256:178c367a61c1039e44e17c56fcc4a6e7dc11b33561261382d419b6ddb4401810"}, + {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, ] ipython = [ - {file = "ipython-7.22.0-py3-none-any.whl", hash = "sha256:c0ce02dfaa5f854809ab7413c601c4543846d9da81010258ecdab299b542d199"}, - {file = "ipython-7.22.0.tar.gz", hash = "sha256:9c900332d4c5a6de534b4befeeb7de44ad0cc42e8327fa41b7685abde58cec74"}, + {file = "ipython-7.25.0-py3-none-any.whl", hash = "sha256:aa21412f2b04ad1a652e30564fff6b4de04726ce875eab222c8430edc6db383a"}, + {file = "ipython-7.25.0.tar.gz", hash = "sha256:54bbd1fe3882457aaf28ae060a5ccdef97f212a741754e420028d4ec5c2291dc"}, ] ipython-genutils = [ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, @@ -1311,16 +1346,16 @@ isodate = [ {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"}, ] isort = [ - {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, - {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, + {file = "isort-5.9.1-py3-none-any.whl", hash = "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"}, + {file = "isort-5.9.1.tar.gz", hash = "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56"}, ] jedi = [ {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, ] jinja2 = [ - {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, - {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, + {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, + {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, ] jsonschema = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, @@ -1368,47 +1403,52 @@ lxml = [ {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"}, ] markupsafe = [ - {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +matplotlib-inline = [ + {file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"}, + {file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, + {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] parso = [ {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, @@ -1431,65 +1471,79 @@ pika = [ {file = "pika-1.2.0.tar.gz", hash = "sha256:f023d6ac581086b124190cb3dc81dd581a149d216fa4540ac34f9be1e3970b89"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.18-py3-none-any.whl", hash = "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04"}, - {file = "prompt_toolkit-3.0.18.tar.gz", hash = "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"}, + {file = "prompt_toolkit-3.0.19-py3-none-any.whl", hash = "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"}, + {file = "prompt_toolkit-3.0.19.tar.gz", hash = "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f"}, ] psycopg2-binary = [ - {file = "psycopg2-binary-2.8.6.tar.gz", hash = "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c"}, - {file = "psycopg2_binary-2.8.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1"}, - {file = "psycopg2_binary-2.8.6-cp34-cp34m-win32.whl", hash = "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2"}, - {file = "psycopg2_binary-2.8.6-cp34-cp34m-win_amd64.whl", hash = "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-win32.whl", hash = "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd"}, - {file = "psycopg2_binary-2.8.6-cp35-cp35m-win_amd64.whl", hash = "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-win32.whl", hash = "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2"}, - {file = "psycopg2_binary-2.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-win32.whl", hash = "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67"}, - {file = "psycopg2_binary-2.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-win32.whl", hash = "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729"}, - {file = "psycopg2_binary-2.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77"}, - {file = "psycopg2_binary-2.8.6-cp39-cp39-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83"}, - {file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52"}, - {file = "psycopg2_binary-2.8.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd"}, + {file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d"}, + {file = "psycopg2_binary-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e"}, + {file = "psycopg2_binary-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-win32.whl", hash = "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32"}, + {file = "psycopg2_binary-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-win32.whl", hash = "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975"}, + {file = "psycopg2_binary-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68"}, ] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] pygments = [ - {file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"}, - {file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"}, + {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, + {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, ] pyjwt = [ {file = "PyJWT-2.1.0-py3-none-any.whl", hash = "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1"}, {file = "PyJWT-2.1.0.tar.gz", hash = "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"}, ] -pyoai = [ - {file = "pyoai-2.5.0.tar.gz", hash = "sha256:029521e1f6a819511feb4299a6181b5c312e8a71f7cddc4547e27001e7552be0"}, -] +pyoai = [] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pyrsistent = [ - {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1"}, + {file = "pyrsistent-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f"}, + {file = "pyrsistent-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2"}, + {file = "pyrsistent-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427"}, + {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef"}, + {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c"}, + {file = "pyrsistent-0.18.0-cp38-cp38-win32.whl", hash = "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78"}, + {file = "pyrsistent-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b"}, + {file = "pyrsistent-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4"}, + {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680"}, + {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426"}, + {file = "pyrsistent-0.18.0-cp39-cp39-win32.whl", hash = "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b"}, + {file = "pyrsistent-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea"}, + {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, ] python-box = [ {file = "python-box-5.3.0.tar.gz", hash = "sha256:4ed4ef5d34de505a65c01e3f1911de8cdb29484fcae0c035141dce535c6c194a"}, @@ -1510,6 +1564,37 @@ pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +pyyaml = [ + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, +] rdflib = [ {file = "rdflib-5.0.0-py3-none-any.whl", hash = "sha256:88208ea971a87886d60ae2b1a4b2cdc263527af0454c422118d43fe64b357877"}, {file = "rdflib-5.0.0.tar.gz", hash = "sha256:78149dd49d385efec3b3adfbd61c87afaf1281c30d3fcaf1b323b34f603fb155"}, @@ -1519,47 +1604,47 @@ redis = [ {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] regex = [ - {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"}, - {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"}, - {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"}, - {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"}, - {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"}, - {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"}, - {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"}, - {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"}, - {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"}, - {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"}, - {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"}, - {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"}, - {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"}, - {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"}, - {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"}, - {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, - {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, + {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, + {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, + {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, + {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, + {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, + {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, + {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, + {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, + {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, + {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, + {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, + {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, + {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, + {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, + {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, + {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, + {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, ] requests = [ {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, @@ -1570,16 +1655,16 @@ responses = [ {file = "responses-0.13.3.tar.gz", hash = "sha256:18a5b88eb24143adbf2b4100f328a2f5bfa72fbdacf12d97d41f07c26c45553d"}, ] six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] snowballstemmer = [ {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, ] sphinx = [ - {file = "Sphinx-3.5.4-py3-none-any.whl", hash = "sha256:2320d4e994a191f4b4be27da514e46b3d6b420f2ff895d064f52415d342461e8"}, - {file = "Sphinx-3.5.4.tar.gz", hash = "sha256:19010b7b9fa0dc7756a6e105b2aacd3a80f798af3c25c273be64d7beeb482cb1"}, + {file = "Sphinx-4.0.3-py3-none-any.whl", hash = "sha256:5747f3c855028076fcff1e4df5e75e07c836f0ac11f7df886747231092cfe4ad"}, + {file = "Sphinx-4.0.3.tar.gz", hash = "sha256:dff357e6a208eb7edb2002714733ac21a9fe597e73609ff417ab8cf0c6b4fbb8"}, ] sphinx-autobuild = [ {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, @@ -1598,8 +1683,8 @@ sphinxcontrib-devhelp = [ {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, ] sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, - {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, ] sphinxcontrib-jsmath = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, @@ -1610,8 +1695,8 @@ sphinxcontrib-qthelp = [ {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, ] sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, - {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] sqlparse = [ {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, @@ -1709,13 +1794,13 @@ typed-ast = [ {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ - {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, - {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, - {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] urllib3 = [ - {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, - {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, + {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, + {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, @@ -1726,6 +1811,6 @@ xmltodict = [ {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, ] zipp = [ - {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, - {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, + {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, + {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, ] diff --git a/pyproject.toml b/pyproject.toml index c5a5cb2c..8742b747 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,13 +22,14 @@ django-environ = "^0.4.5" django-split-settings = "^1.0.1" rdflib = "^5.0.0" python-dateutil = "^2.8.1" -pyoai = "^2.5.0" +pyoai = {git = "https://github.com/infrae/pyoai", rev = "5f6eba12"} +python-box = "^5.3.0" python-simplexquery = {version = "*", optional = true} # These are here because of: https://github.com/python-poetry/poetry/issues/1644 -Sphinx = {version = "^3.5.4", optional = true} +Sphinx = {version = "^4.0.2", optional = true} sphinx-autobuild = {version = "^2021.3.14", optional = true} sphinx-rtd-theme = {version = "^0.5.2", optional = true} -python-box = "^5.3.0" +PyYAML = { version = "^5.4.1", optional = true} [tool.poetry.dev-dependencies] responses = "^0.13.2" @@ -46,6 +47,7 @@ coverage = {version = "^5.5", extras = ["toml"]} [tool.poetry.extras] simplexquery = ["python-simplexquery"] docs = ["Sphinx", "sphinx-autobuild", "sphinx-rtd-theme"] +swagger = ["PyYAML"] [tool.isort] profile = "black" @@ -54,11 +56,11 @@ known_first_party = ["metax_api", "src/metax_api"] skip_glob = "*/models/__init__.py,__init__.py" include_trailing_comma = true combine_as_imports = true -line_length = 120 +line_length = 100 [tool.black] target-version = ['py38'] -line-length = 120 +line-length = 100 exclude = "/migrations/" [build-system] diff --git a/requirements.txt b/requirements.txt index cf0e112f..f7f3a93c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,72 +1,95 @@ +alabaster==0.7.12; python_version >= "3.6" appdirs==1.4.4; python_version >= "3.6" appnope==0.1.2; sys_platform == "darwin" and python_version >= "3.7" -asgiref==3.3.4; python_version >= "3.6" +asgiref==3.4.1; python_version >= "3.6" asttokens==2.0.5 -attrs==20.3.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" +attrs==21.2.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" autosemver==0.5.5 +babel==2.9.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" backcall==0.2.0; python_version >= "3.7" black==20.8b1; python_version >= "3.6" -certifi==2020.12.5; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" +certifi==2021.5.30; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" chardet==4.0.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -click==7.1.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" -colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0" -datacite==1.1.1 -decorator==5.0.7; python_version >= "3.7" +click==8.0.1; python_version >= "3.6" +colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" and platform_system == "Windows" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0" and platform_system == "Windows" +coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") +datacite==1.1.2 +decorator==5.0.9; python_version >= "3.7" django-debug-toolbar==3.2.1; python_version >= "3.6" django-environ==0.4.5 django-rainbowtests==0.6.0 django-split-settings==1.0.1; python_version >= "3.6" and python_version < "4.0" django-watchman==1.2.0 -django==3.1.8; python_version >= "3.6" +django==3.1.13; python_version >= "3.6" djangorestframework==3.12.4; python_version >= "3.5" +docutils==0.16; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" dulwich==0.19.16 -elasticsearch==7.12.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0" and python_version < "4") -executing==0.6.0 +elasticsearch==7.13.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0" and python_version < "4") +executing==0.7.0 gunicorn==20.1.0; python_version >= "3.5" -icecream==2.1.0 +icecream==2.1.1 idna==2.10; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" idutils==1.1.8 -importlib-metadata==4.0.1; python_version >= "3.6" and python_version < "3.8" -ipdb==0.13.7; python_version >= "2.7" +imagesize==1.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +importlib-metadata==4.6.1; python_version >= "3.6" and python_version < "3.8" +ipdb==0.13.9; python_version >= "2.7" ipython-genutils==0.2.0; python_version >= "3.7" -ipython==7.22.0; python_version >= "3.7" +ipython==7.25.0; python_version >= "3.7" isbnid-fork==0.5.2 isodate==0.6.0 +isort==5.9.1; python_full_version >= "3.6.1" and python_version < "4.0" jedi==0.18.0; python_version >= "3.7" +jinja2==3.0.1; python_version >= "3.6" jsonschema==3.2.0 +livereload==2.6.3; python_version >= "3.6" lxml==4.6.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +markupsafe==2.0.1; python_version >= "3.6" +matplotlib-inline==0.1.2; python_version >= "3.7" mypy-extensions==0.4.3; python_version >= "3.6" +packaging==21.0; python_version >= "3.6" parso==0.8.2; python_version >= "3.7" pathspec==0.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.7" pickleshare==0.7.5; python_version >= "3.7" pika==1.2.0 -prompt-toolkit==3.0.18; python_full_version >= "3.6.1" and python_version >= "3.7" -psycopg2-binary==2.8.6; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") +prompt-toolkit==3.0.19; python_full_version >= "3.6.1" and python_version >= "3.7" +psycopg2-binary==2.9.1; python_version >= "3.6" ptyprocess==0.7.0; sys_platform != "win32" and python_version >= "3.7" -pygments==2.8.1; python_version >= "3.7" +pygments==2.9.0; python_version >= "3.7" pyjwt==2.1.0; python_version >= "3.6" -pyoai==2.5.0 -pyparsing==2.4.7; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" -pyrsistent==0.17.3; python_version >= "3.5" +pyoai @ git+https://github.com/infrae/pyoai@5f6eba12 +pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" +pyrsistent==0.18.0; python_version >= "3.6" python-box==5.3.0; python_version >= "3.6" python-dateutil==2.8.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") python-simplexquery==1.0.5.3 -pytz==2021.1; python_version >= "3.6" +pytz==2021.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +pyyaml==5.4.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") rdflib==5.0.0 redis==3.5.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -regex==2021.4.4; python_version >= "3.6" -requests==2.25.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +regex==2021.7.6; python_version >= "3.6" +requests==2.25.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" responses==0.13.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -six==1.15.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +snowballstemmer==2.1.0; python_version >= "3.6" +sphinx-autobuild==2021.3.14; python_version >= "3.6" +sphinx-rtd-theme==0.5.2 +sphinx==4.0.3; python_version >= "3.6" +sphinxcontrib-applehelp==1.0.2; python_version >= "3.6" +sphinxcontrib-devhelp==1.0.2; python_version >= "3.6" +sphinxcontrib-htmlhelp==2.0.0; python_version >= "3.6" +sphinxcontrib-jsmath==1.0.1; python_version >= "3.6" +sphinxcontrib-qthelp==1.0.3; python_version >= "3.6" +sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.6" sqlparse==0.4.1; python_version >= "3.6" structlog==21.1.0; python_version >= "3.6" tblib==1.7.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -toml==0.10.2; python_version > "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version > "3.6" +toml==0.10.2; python_version > "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version > "3.6" and python_version < "4" +tornado==6.1; python_version >= "3.6" traitlets==5.0.5; python_version >= "3.7" typed-ast==1.4.3; python_version >= "3.6" -typing-extensions==3.7.4.3; python_version < "3.8" and python_version >= "3.6" -urllib3==1.26.4; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" +typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6" +urllib3==1.26.6; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" wcwidth==0.2.5; python_full_version >= "3.6.1" and python_version >= "3.7" xmltodict==0.12.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") -zipp==3.4.1; python_version >= "3.6" and python_version < "3.8" +zipp==3.5.0; python_version >= "3.6" and python_version < "3.8" diff --git a/sphinx.dockerfile b/sphinx.dockerfile new file mode 100644 index 00000000..71fd475c --- /dev/null +++ b/sphinx.dockerfile @@ -0,0 +1,10 @@ +FROM python:3.8-alpine + +RUN pip install Sphinx sphinx-autobuild sphinx-rtd-theme +RUN mkdir -p /sphinx/build + +WORKDIR /sphinx + +EXPOSE 8000 + +CMD ["sphinx-autobuild", "--host", "0.0.0.0", "-E", "-j", "auto", "/sphinx/", "/sphinx/build/"] diff --git a/src/.coveragerc b/src/.coveragerc index df33cfa7..63a369dc 100755 --- a/src/.coveragerc +++ b/src/.coveragerc @@ -12,6 +12,7 @@ omit = metax_api/onappstart.py branch = True concurrency = multiprocessing +data_file = coverage_data/coverage.db [report] exclude_lines = diff --git a/src/metax_api/__init__.py b/src/metax_api/__init__.py index 2ace85dc..60ef1371 100755 --- a/src/metax_api/__init__.py +++ b/src/metax_api/__init__.py @@ -5,4 +5,4 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -default_app_config = 'metax_api.onappstart.OnAppStart' +default_app_config = "metax_api.onappstart.OnAppStart" diff --git a/src/metax_api/api/oaipmh/base/metax_oai_server.py b/src/metax_api/api/oaipmh/base/metax_oai_server.py index f6a83b83..2fd78634 100755 --- a/src/metax_api/api/oaipmh/base/metax_oai_server.py +++ b/src/metax_api/api/oaipmh/base/metax_oai_server.py @@ -11,44 +11,53 @@ from django.utils import timezone from oaipmh import common from oaipmh.common import ResumptionOAIPMH -from oaipmh.error import BadArgumentError, CannotDisseminateFormatError, IdDoesNotExistError, NoRecordsMatchError +from oaipmh.error import ( + BadArgumentError, + CannotDisseminateFormatError, + IdDoesNotExistError, + NoRecordsMatchError, +) from metax_api.models.catalog_record import CatalogRecord, DataCatalog from metax_api.services import CatalogRecordService as CRS from metax_api.services.datacite_service import DataciteException, convert_cr_to_datacite_cr_json # SYKE_IDENTIFIER_PREFIX -SYKE_URL_PREFIX_TEMPLATE = 'https://metadata.ymparisto.fi/dataset/%s' -DATACATALOGS_SET = 'datacatalogs' -DATASETS_SET = 'datasets' -OAI_DC_MDPREFIX = 'oai_dc' -OAI_DATACITE_MDPREFIX = 'oai_datacite' -OAI_FAIRDATA_DATACITE_MDPREFIX = 'oai_fairdata_datacite' -OAI_DC_URNRESOLVER_MDPREFIX = 'oai_dc_urnresolver' +SYKE_URL_PREFIX_TEMPLATE = "https://metadata.ymparisto.fi/dataset/%s" +DATACATALOGS_SET = "datacatalogs" +DATASETS_SET = "datasets" +OAI_DC_MDPREFIX = "oai_dc" +OAI_DATACITE_MDPREFIX = "oai_datacite" +OAI_FAIRDATA_DATACITE_MDPREFIX = "oai_fairdata_datacite" +OAI_DC_URNRESOLVER_MDPREFIX = "oai_dc_urnresolver" class MetaxOAIServer(ResumptionOAIPMH): - def _validate_mdprefix_and_set(self, metadataPrefix, set=None): if not set: pass elif set == DATACATALOGS_SET: if metadataPrefix != OAI_DC_MDPREFIX: - raise BadArgumentError('Invalid metadataPrefix value. Data catalogs can only be harvested using ' - '{0} format.'.format(OAI_DC_MDPREFIX)) - elif set in settings.OAI['SET_MAPPINGS']: + raise BadArgumentError( + "Invalid metadataPrefix value. Data catalogs can only be harvested using " + "{0} format.".format(OAI_DC_MDPREFIX) + ) + elif set in settings.OAI["SET_MAPPINGS"]: if set != DATASETS_SET and metadataPrefix == OAI_DC_URNRESOLVER_MDPREFIX: - raise BadArgumentError('When using metadataPrefix {0}, set value must be either {1} or {2}' - .format(OAI_DC_URNRESOLVER_MDPREFIX, DATACATALOGS_SET, DATASETS_SET)) + raise BadArgumentError( + "When using metadataPrefix {0}, set value must be either {1} or {2}".format( + OAI_DC_URNRESOLVER_MDPREFIX, DATACATALOGS_SET, DATASETS_SET + ) + ) else: - raise BadArgumentError('Invalid set value') + raise BadArgumentError("Invalid set value") @staticmethod def _get_default_set_filter(): # there are not that many sets yet, so just using list even though # there will be duplicates catalog_urns = [] - for k, v in settings.OAI['SET_MAPPINGS'].items(): + for k, v in settings.OAI["SET_MAPPINGS"].items(): catalog_urns.extend(v) return catalog_urns @@ -70,25 +79,36 @@ def _get_urnresolver_record_data(self, set, cursor, batch_size, from_=None, unti # Fetch only needed values as dict to increase performance. records = records.values( - 'identifier', - 'date_created', - 'date_modified', - 'data_catalog__catalog_json', - 'research_dataset') + "identifier", + "date_created", + "date_modified", + "data_catalog__catalog_json", + "research_dataset", + ) data = [] for record in records: metadatas = self._get_oai_dc_urnresolver_metadatas_for_record(record) for md in metadatas: - item = (common.Header('', self._get_record_identifier(record, set), - self._get_header_timestamp(record), ['metax'], False), - common.Metadata('', md), None) + item = ( + common.Header( + "", + self._get_record_identifier(record, set), + self._get_header_timestamp(record), + ["metax"], + False, + ), + common.Metadata("", md), + None, + ) data.append(item) cursor_end = cursor + batch_size if cursor + batch_size < len(data) else len(data) return data[cursor:cursor_end] - def _get_filtered_records_data(self, verb, metadata_prefix, set, cursor, batch_size, from_=None, until=None): + def _get_filtered_records_data( + self, verb, metadata_prefix, set, cursor, batch_size, from_=None, until=None + ): proxy = CatalogRecord if set == DATACATALOGS_SET: proxy = DataCatalog @@ -108,25 +128,43 @@ def _get_filtered_records_data(self, verb, metadata_prefix, set, cursor, batch_s pass else: query_set = query_set.filter( - data_catalog__catalog_json__identifier__in=settings.OAI['SET_MAPPINGS'][set]) + data_catalog__catalog_json__identifier__in=settings.OAI["SET_MAPPINGS"][set] + ) else: - query_set = query_set.filter(data_catalog__catalog_json__identifier__in=self._get_default_set_filter()) - query_set = query_set.filter(state='published') + query_set = query_set.filter( + data_catalog__catalog_json__identifier__in=self._get_default_set_filter() + ) + query_set = query_set.filter(state="published") data = [] for record in query_set: - if verb == 'ListRecords': + if verb == "ListRecords": try: - oai_item = self._get_oai_item(self._get_record_identifier(record, set), record, metadata_prefix) + oai_item = self._get_oai_item( + self._get_record_identifier(record, set), + record, + metadata_prefix, + ) data.append(oai_item) except CannotDisseminateFormatError as e: - if metadata_prefix == OAI_FAIRDATA_DATACITE_MDPREFIX or metadata_prefix == OAI_DATACITE_MDPREFIX: + if ( + metadata_prefix == OAI_FAIRDATA_DATACITE_MDPREFIX + or metadata_prefix == OAI_DATACITE_MDPREFIX + ): pass else: raise e - elif verb == 'ListIdentifiers': + elif verb == "ListIdentifiers": identifier = self._get_record_identifier(record, set) - data.append(common.Header('', identifier, self._get_header_timestamp(record), ['metax'], False)) + data.append( + common.Header( + "", + identifier, + self._get_header_timestamp(record), + ["metax"], + False, + ) + ) else: raise Exception("OAI-PMH bad code error") @@ -135,11 +173,11 @@ def _get_filtered_records_data(self, verb, metadata_prefix, set, cursor, batch_s def _handle_syke_urnresolver_metadata(self, record): identifiers = [] - preferred_identifier = record.research_dataset.get('preferred_identifier') + preferred_identifier = record.research_dataset.get("preferred_identifier") identifiers.append(preferred_identifier) - for id_obj in record.research_dataset.get('other_identifier', []): - if id_obj.get('notation', '').startswith('{'): - uuid = id_obj['notation'] + for id_obj in record.research_dataset.get("other_identifier", []): + if id_obj.get("notation", "").startswith("{"): + uuid = id_obj["notation"] identifiers.append(SYKE_URL_PREFIX_TEMPLATE % uuid) break return identifiers @@ -156,76 +194,84 @@ def _get_oai_dc_urnresolver_metadatas_for_record(self, record): metadatas = [] if isinstance(record, dict): - pref_id = record['research_dataset'].get('preferred_identifier') - dc_id = record['data_catalog__catalog_json'].get('identifier') - is_harvested = record['data_catalog__catalog_json'].get('harvested', False) is True - if record['research_dataset'].get('other_identifier') is not None: - other_ids = record['research_dataset'].get('other_identifier') + pref_id = record["research_dataset"].get("preferred_identifier") + dc_id = record["data_catalog__catalog_json"].get("identifier") + is_harvested = record["data_catalog__catalog_json"].get("harvested", False) is True + if record["research_dataset"].get("other_identifier") is not None: + other_ids = record["research_dataset"].get("other_identifier") else: other_ids = [] - if dc_id == 'urn:nbn:fi:att:data-catalog-harvest-syke': + if dc_id == "urn:nbn:fi:att:data-catalog-harvest-syke": for id_obj in other_ids: - if id_obj.get('notation', '').startswith('{'): - metadatas.append({'identifier': [SYKE_URL_PREFIX_TEMPLATE % id_obj['notation'], pref_id]}) + if id_obj.get("notation", "").startswith("{"): + metadatas.append( + { + "identifier": [ + SYKE_URL_PREFIX_TEMPLATE % id_obj["notation"], + pref_id, + ] + } + ) break elif dc_id not in settings.LEGACY_CATALOGS: - resolution_url = settings.OAI['ETSIN_URL_TEMPLATE'] % record['identifier'] - if not is_harvested and (pref_id.startswith('urn:nbn:fi:att:') or - pref_id.startswith('urn:nbn:fi:csc')): - metadatas.append({'identifier': [resolution_url, pref_id]}) + resolution_url = settings.OAI["ETSIN_URL_TEMPLATE"] % record["identifier"] + if not is_harvested and ( + pref_id.startswith("urn:nbn:fi:att:") or pref_id.startswith("urn:nbn:fi:csc") + ): + metadatas.append({"identifier": [resolution_url, pref_id]}) for id_obj in other_ids: - if id_obj.get('notation', '').startswith('urn:nbn:fi:csc-kata'): - metadatas.append({'identifier': [resolution_url, id_obj['notation']]}) + if id_obj.get("notation", "").startswith("urn:nbn:fi:csc-kata"): + metadatas.append({"identifier": [resolution_url, id_obj["notation"]]}) return metadatas def _get_oaic_dc_value(self, value, lang=None): valueDict = {} - valueDict['value'] = value + valueDict["value"] = value if lang: - valueDict['lang'] = lang + valueDict["lang"] = lang return valueDict def _get_oai_dc_metadata(self, record, json): identifier = [] - if 'preferred_identifier' in json: - identifier.append(self._get_oaic_dc_value(json.get('preferred_identifier'))) - if 'identifier' in json: - identifier.append(self._get_oaic_dc_value(json.get('identifier'))) + if "preferred_identifier" in json: + identifier.append(self._get_oaic_dc_value(json.get("preferred_identifier"))) + if "identifier" in json: + identifier.append(self._get_oaic_dc_value(json.get("identifier"))) title = [] - title_data = json.get('title', {}) + title_data = json.get("title", {}) for key, value in title_data.items(): title.append(self._get_oaic_dc_value(value, key)) creator = [] - creator_data = json.get('creator', []) + creator_data = json.get("creator", []) for value in creator_data: - if 'name' in value: - if isinstance(value['name'], dict): - for key, val in value['name'].items(): + if "name" in value: + if isinstance(value["name"], dict): + for key, val in value["name"].items(): creator.append(self._get_oaic_dc_value(val, key)) else: - creator.append(self._get_oaic_dc_value(value.get('name'))) + creator.append(self._get_oaic_dc_value(value.get("name"))) subject = [] - subject_data = json.get('keyword', []) + subject_data = json.get("keyword", []) for value in subject_data: subject.append(self._get_oaic_dc_value(value)) - subject_data = json.get('field_of_science', []) + subject_data = json.get("field_of_science", []) for value in subject_data: - for key, value2 in value.get('pref_label', {}).items(): + for key, value2 in value.get("pref_label", {}).items(): subject.append(self._get_oaic_dc_value(value2, key)) - subject_data = json.get('theme', []) + subject_data = json.get("theme", []) for value in subject_data: - for key, value2 in value.get('pref_label', {}).items(): + for key, value2 in value.get("pref_label", {}).items(): subject.append(self._get_oaic_dc_value(value2, key)) desc = [] - desc_data = json.get('description', None) + desc_data = json.get("description", None) if desc_data is not None: if isinstance(desc_data, dict): for key, value in desc_data.items(): @@ -234,71 +280,71 @@ def _get_oai_dc_metadata(self, record, json): desc.append(desc_data) publisher = [] - publisher_data = json.get('publisher', {}) - for key, value in publisher_data.get('name', {}).items(): + publisher_data = json.get("publisher", {}) + for key, value in publisher_data.get("name", {}).items(): publisher.append(self._get_oaic_dc_value(value, key)) contributor = [] - contributor_data = json.get('contributor', []) + contributor_data = json.get("contributor", []) for value in contributor_data: - if 'name' in value: - if isinstance(value['name'], dict): - for key, val in value['name'].items(): + if "name" in value: + if isinstance(value["name"], dict): + for key, val in value["name"].items(): contributor.append(self._get_oaic_dc_value(val, key)) else: - contributor.append(self._get_oaic_dc_value(value.get('name'))) + contributor.append(self._get_oaic_dc_value(value.get("name"))) date = self._get_oaic_dc_value(str(record.date_created)) language = [] - language_data = json.get('language', []) + language_data = json.get("language", []) for value in language_data: - if 'identifier' in value: - language.append(self._get_oaic_dc_value(value['identifier'])) + if "identifier" in value: + language.append(self._get_oaic_dc_value(value["identifier"])) relation = [] - relation_data = json.get('relation', []) + relation_data = json.get("relation", []) for value in relation_data: - if 'identifier' in value.get('entity', {}): - relation.append(self._get_oaic_dc_value(value['entity']['identifier'])) + if "identifier" in value.get("entity", {}): + relation.append(self._get_oaic_dc_value(value["entity"]["identifier"])) coverage = [] - coverage_data = json.get('spatial', []) + coverage_data = json.get("spatial", []) for value in coverage_data: - if 'geographic_name' in value: - coverage.append(self._get_oaic_dc_value(value['geographic_name'])) + if "geographic_name" in value: + coverage.append(self._get_oaic_dc_value(value["geographic_name"])) rights = [] - rights_data = json.get('access_rights', {}) - rights_desc = rights_data.get('description', {}).get('name', {}) + rights_data = json.get("access_rights", {}) + rights_desc = rights_data.get("description", {}).get("name", {}) for key, value in rights_desc.items(): rights.append(self._get_oaic_dc_value(value, key)) - for value in rights_data.get('license', []): - if 'identifier' in value: - rights.append(self._get_oaic_dc_value(value['identifier'])) + for value in rights_data.get("license", []): + if "identifier" in value: + rights.append(self._get_oaic_dc_value(value["identifier"])) if isinstance(record, CatalogRecord): - m_type = 'Dataset' + m_type = "Dataset" elif isinstance(record, DataCatalog): - m_type = 'Datacatalog' + m_type = "Datacatalog" else: - m_type = 'N/A' + m_type = "N/A" meta = { - 'identifier': identifier, - 'title': title, - 'creator': creator, - 'subject': subject, - 'description': desc, - 'publisher': publisher, - 'contributor': contributor, - 'date': [date], - 'type': [self._get_oaic_dc_value(m_type)], - 'language': language, - 'relation': relation, - 'coverage': coverage, - 'rights': rights + "identifier": identifier, + "title": title, + "creator": creator, + "subject": subject, + "description": desc, + "publisher": publisher, + "contributor": contributor, + "date": [date], + "type": [self._get_oaic_dc_value(m_type)], + "language": language, + "relation": relation, + "coverage": coverage, + "rights": rights, } return meta @@ -310,9 +356,9 @@ def _get_oai_datacite_metadata(self, cr, datacite_type): raise CannotDisseminateFormatError(str(e)) meta = { - 'datacentreSymbol': 'Metax', - 'schemaVersion': '4.1', - 'payload': datacite_xml + "datacentreSymbol": "Metax", + "schemaVersion": "4.1", + "payload": datacite_xml, } return meta @@ -328,9 +374,9 @@ def _get_metadata_for_record(self, record, metadataPrefix): if metadataPrefix == OAI_DC_MDPREFIX: meta = self._get_oai_dc_metadata(record, json) elif metadataPrefix == OAI_FAIRDATA_DATACITE_MDPREFIX: - meta = self._get_oai_datacite_metadata(record, 'fairdata_datacite') + meta = self._get_oai_datacite_metadata(record, "fairdata_datacite") elif metadataPrefix == OAI_DATACITE_MDPREFIX: - meta = self._get_oai_datacite_metadata(record, 'datacite') + meta = self._get_oai_datacite_metadata(record, "datacite") return self._fix_metadata(meta) @@ -339,15 +385,18 @@ def _get_header_timestamp(self, record): Can handle record as json or object. """ if isinstance(record, dict): - modified = record.get('date_modified', None) - timestamp = modified if modified is not None else record['date_created'] + modified = record.get("date_modified", None) + timestamp = modified if modified is not None else record["date_created"] else: timestamp = record.date_modified if record.date_modified else record.date_created return timezone.make_naive(timestamp) def _get_oai_item(self, identifier, record, metadata_prefix): - item = (common.Header('', identifier, self._get_header_timestamp(record), ['metax'], False), - common.Metadata('', self._get_metadata_for_record(record, metadata_prefix)), None) + item = ( + common.Header("", identifier, self._get_header_timestamp(record), ["metax"], False), + common.Metadata("", self._get_metadata_for_record(record, metadata_prefix)), + None, + ) return item def _fix_metadata(self, meta): @@ -366,91 +415,128 @@ def _get_record_identifier(self, record, set): Can handle record as json or object. """ if set == DATACATALOGS_SET: - return record['catalog_json__identifier'] if isinstance(record, dict) else record.catalog_json['identifier'] + return ( + record["catalog_json__identifier"] + if isinstance(record, dict) + else record.catalog_json["identifier"] + ) else: - return record['identifier'] if isinstance(record, dict) else record.identifier + return record["identifier"] if isinstance(record, dict) else record.identifier -# OAI-PMH VERBS + # OAI-PMH VERBS def identify(self): """Implement OAI-PMH verb Identify .""" - first = CatalogRecord.objects.filter( - data_catalog__catalog_json__identifier__in=self._get_default_set_filter() - ).order_by( - 'date_created' - ).values_list('date_created', flat=True).first() + first = ( + CatalogRecord.objects.filter( + data_catalog__catalog_json__identifier__in=self._get_default_set_filter() + ) + .order_by("date_created") + .values_list("date_created", flat=True) + .first() + ) if first: first = timezone.make_naive(first) else: first = datetime.datetime.now() return common.Identify( - repositoryName=settings.OAI['REPOSITORY_NAME'], - baseURL=settings.OAI['BASE_URL'], + repositoryName=settings.OAI["REPOSITORY_NAME"], + baseURL=settings.OAI["BASE_URL"], protocolVersion="2.0", - adminEmails=[settings.OAI['ADMIN_EMAIL']], + adminEmails=[settings.OAI["ADMIN_EMAIL"]], earliestDatestamp=first, - deletedRecord='no', - granularity='YYYY-MM-DDThh:mm:ssZ', - compression=['identity']) + deletedRecord="no", + granularity="YYYY-MM-DDThh:mm:ssZ", + compression=["identity"], + ) def listMetadataFormats(self, identifier=None): """Implement OAI-PMH verb listMetadataFormats .""" - return [(OAI_DC_MDPREFIX, - 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd', - 'http://www.openarchives.org/OAI/2.0/oai_dc/'), - (OAI_FAIRDATA_DATACITE_MDPREFIX, - 'https://schema.datacite.org/meta/kernel-4.1/metadata.xsd', - 'https://schema.datacite.org/meta/kernel-4.1/'), - (OAI_DATACITE_MDPREFIX, - 'https://schema.datacite.org/meta/kernel-4.1/metadata.xsd', - 'https://schema.datacite.org/meta/kernel-4.1/'), - (OAI_DC_URNRESOLVER_MDPREFIX, - '', - '') - ] + return [ + ( + OAI_DC_MDPREFIX, + "http://www.openarchives.org/OAI/2.0/oai_dc.xsd", + "http://www.openarchives.org/OAI/2.0/oai_dc/", + ), + ( + OAI_FAIRDATA_DATACITE_MDPREFIX, + "https://schema.datacite.org/meta/kernel-4.1/metadata.xsd", + "https://schema.datacite.org/meta/kernel-4.1/", + ), + ( + OAI_DATACITE_MDPREFIX, + "https://schema.datacite.org/meta/kernel-4.1/metadata.xsd", + "https://schema.datacite.org/meta/kernel-4.1/", + ), + (OAI_DC_URNRESOLVER_MDPREFIX, "", ""), + ] def listSets(self, cursor=None, batch_size=None): """Implement OAI-PMH verb ListSets.""" - data = [(DATACATALOGS_SET, DATACATALOGS_SET, '')] - for set_key in settings.OAI['SET_MAPPINGS'].keys(): - data.append((set_key, set_key, '')) + data = [(DATACATALOGS_SET, DATACATALOGS_SET, "")] + for set_key in settings.OAI["SET_MAPPINGS"].keys(): + data.append((set_key, set_key, "")) return data - def listIdentifiers(self, metadataPrefix=None, set=None, cursor=None, - from_=None, until=None, batch_size=None): + def listIdentifiers( + self, + metadataPrefix=None, + set=None, + cursor=None, + from_=None, + until=None, + batch_size=None, + ): """Implement OAI-PMH verb listIdentifiers.""" if metadataPrefix == OAI_DC_URNRESOLVER_MDPREFIX: - raise BadArgumentError('Invalid metadataPrefix value. It can be only used with ListRecords verb') + raise BadArgumentError( + "Invalid metadataPrefix value. It can be only used with ListRecords verb" + ) self._validate_mdprefix_and_set(metadataPrefix, set) - return self._get_filtered_records_data('ListIdentifiers', metadataPrefix, set, cursor, batch_size, from_, until) - - def listRecords(self, metadataPrefix=None, set=None, cursor=None, from_=None, - until=None, batch_size=None): + return self._get_filtered_records_data( + "ListIdentifiers", metadataPrefix, set, cursor, batch_size, from_, until + ) + + def listRecords( + self, + metadataPrefix=None, + set=None, + cursor=None, + from_=None, + until=None, + batch_size=None, + ): """Implement OAI-PMH verb ListRecords.""" self._validate_mdprefix_and_set(metadataPrefix, set) data = [] if metadataPrefix == OAI_DC_URNRESOLVER_MDPREFIX: data = self._get_urnresolver_record_data(set, cursor, batch_size, from_, until) else: - data = self._get_filtered_records_data('ListRecords', metadataPrefix, set, cursor, batch_size, from_, until) + data = self._get_filtered_records_data( + "ListRecords", metadataPrefix, set, cursor, batch_size, from_, until + ) return data def getRecord(self, metadataPrefix, identifier): """Implement OAI-PMH verb GetRecord.""" try: if metadataPrefix == OAI_DC_URNRESOLVER_MDPREFIX: - raise BadArgumentError('Invalid metadataPrefix value. It can be only used with ListRecords verb') + raise BadArgumentError( + "Invalid metadataPrefix value. It can be only used with ListRecords verb" + ) record = CatalogRecord.objects.get(identifier__exact=identifier) - if record.state == 'draft': + if record.state == "draft": raise IdDoesNotExistError("No record with identifier %s is available." % identifier) except CatalogRecord.DoesNotExist: try: record = DataCatalog.objects.get(catalog_json__identifier__exact=identifier) if record and metadataPrefix != OAI_DC_MDPREFIX: - raise BadArgumentError('Invalid metadataPrefix value. Data catalogs can only be harvested using ' - '{0} format.'.format(OAI_DC_MDPREFIX)) + raise BadArgumentError( + "Invalid metadataPrefix value. Data catalogs can only be harvested using " + "{0} format.".format(OAI_DC_MDPREFIX) + ) except DataCatalog.DoesNotExist: raise IdDoesNotExistError("No record with identifier %s is available." % identifier) @@ -459,5 +545,8 @@ def getRecord(self, metadataPrefix, identifier): if metadata is None: raise NoRecordsMatchError - return (common.Header('', identifier, self._get_header_timestamp(record), ['metax'], False), - common.Metadata('', metadata), None) \ No newline at end of file + return ( + common.Header("", identifier, self._get_header_timestamp(record), ["metax"], False), + common.Metadata("", metadata), + None, + ) diff --git a/src/metax_api/api/oaipmh/base/view.py b/src/metax_api/api/oaipmh/base/view.py index 96c0b6ad..ae29a2c4 100755 --- a/src/metax_api/api/oaipmh/base/view.py +++ b/src/metax_api/api/oaipmh/base/view.py @@ -20,72 +20,93 @@ MetaxOAIServer, ) -NS_OAIDC_DATACITE = 'http://schema.datacite.org/oai/oai-1.0/' +NS_OAIDC_DATACITE = "http://schema.datacite.org/oai/oai-1.0/" -NS_OAIPMH = 'http://www.openarchives.org/OAI/2.0/' -NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance' -NS_OAIDC = 'http://www.openarchives.org/OAI/2.0/oai_dc/' +NS_OAIPMH = "http://www.openarchives.org/OAI/2.0/" +NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" +NS_OAIDC = "http://www.openarchives.org/OAI/2.0/oai_dc/" NS_DC = "http://purl.org/dc/elements/1.1/" NS_XML = "http://www.w3.org/XML/1998/namespace" def nsoai(name): - return '{%s}%s' % (NS_OAIPMH, name) + return "{%s}%s" % (NS_OAIPMH, name) def nsoaidc(name): - return '{%s}%s' % (NS_OAIDC, name) + return "{%s}%s" % (NS_OAIDC, name) def nsdc(name): - return '{%s}%s' % (NS_DC, name) + return "{%s}%s" % (NS_DC, name) def oai_dc_writer_with_lang(element, metadata): - e_dc = SubElement(element, nsoaidc('dc'), - nsmap={'oai_dc': NS_OAIDC, 'dc': NS_DC, 'xsi': NS_XSI, 'xml': NS_XML}) - e_dc.set('{%s}schemaLocation' % NS_XSI, - '%s http://www.openarchives.org/OAI/2.0/oai_dc.xsd' % NS_DC) + e_dc = SubElement( + element, + nsoaidc("dc"), + nsmap={"oai_dc": NS_OAIDC, "dc": NS_DC, "xsi": NS_XSI, "xml": NS_XML}, + ) + e_dc.set( + "{%s}schemaLocation" % NS_XSI, + "%s http://www.openarchives.org/OAI/2.0/oai_dc.xsd" % NS_DC, + ) map = metadata.getMap() for name in [ - 'title', 'creator', 'subject', 'description', 'publisher', - 'contributor', 'date', 'type', 'format', 'identifier', - 'source', 'language', 'relation', 'coverage', 'rights' + "title", + "creator", + "subject", + "description", + "publisher", + "contributor", + "date", + "type", + "format", + "identifier", + "source", + "language", + "relation", + "coverage", + "rights", ]: try: for value in map.get(name, []): e = SubElement(e_dc, nsdc(name)) - if 'lang' in value: - e.attrib['{http://www.w3.org/XML/1998/namespace}lang'] = value['lang'] - e.text = value['value'] + if "lang" in value: + e.attrib["{http://www.w3.org/XML/1998/namespace}lang"] = value["lang"] + e.text = value["value"] except: pass def oai_fairdata_datacite_writer(element, metadata): e_dc = SubElement(element, OAI_FAIRDATA_DATACITE_MDPREFIX, nsmap={None: NS_OAIDC_DATACITE}) - e_dc.set('{%s}schemaLocation' % NS_XSI, - 'http://schema.datacite.org/oai/oai-1.0/ oai_datacite.xsd') + e_dc.set( + "{%s}schemaLocation" % NS_XSI, + "http://schema.datacite.org/oai/oai-1.0/ oai_datacite.xsd", + ) - e = SubElement(e_dc, 'schemaVersion') - e.text = metadata['schemaVersion'][0] - e = SubElement(e_dc, 'datacentreSymbol') - e.text = metadata['datacentreSymbol'][0] - e = SubElement(e_dc, 'payload') - e.append(etree.fromstring(metadata['payload'][0])) + e = SubElement(e_dc, "schemaVersion") + e.text = metadata["schemaVersion"][0] + e = SubElement(e_dc, "datacentreSymbol") + e.text = metadata["datacentreSymbol"][0] + e = SubElement(e_dc, "payload") + e.append(etree.fromstring(metadata["payload"][0])) def oai_datacite_writer(element, metadata): e_dc = SubElement(element, OAI_DATACITE_MDPREFIX, nsmap={None: NS_OAIDC_DATACITE}) - e_dc.set('{%s}schemaLocation' % NS_XSI, - 'http://schema.datacite.org/oai/oai-1.0/ oai_datacite.xsd') + e_dc.set( + "{%s}schemaLocation" % NS_XSI, + "http://schema.datacite.org/oai/oai-1.0/ oai_datacite.xsd", + ) - e = SubElement(e_dc, 'schemaVersion') - e.text = metadata['schemaVersion'][0] - e = SubElement(e_dc, 'datacentreSymbol') - e.text = metadata['datacentreSymbol'][0] - e = SubElement(e_dc, 'payload') - e.append(etree.fromstring(metadata['payload'][0])) + e = SubElement(e_dc, "schemaVersion") + e.text = metadata["schemaVersion"][0] + e = SubElement(e_dc, "datacentreSymbol") + e.text = metadata["datacentreSymbol"][0] + e = SubElement(e_dc, "payload") + e.append(etree.fromstring(metadata["payload"][0])) def oaipmh_view(request): @@ -96,9 +117,11 @@ def oaipmh_view(request): metadata_registry.registerWriter(OAI_FAIRDATA_DATACITE_MDPREFIX, oai_fairdata_datacite_writer) metadata_registry.registerWriter(OAI_DATACITE_MDPREFIX, oai_datacite_writer) - server = oaiserver.BatchingServer(metax_server, - metadata_registry=metadata_registry, - resumption_batch_size=settings.OAI['BATCH_SIZE']) + server = oaiserver.BatchingServer( + metax_server, + metadata_registry=metadata_registry, + resumption_batch_size=settings.OAI["BATCH_SIZE"], + ) xml = server.handleRequest(request.GET.dict()) - return HttpResponse(xml, content_type='text/xml; charset=utf-8') + return HttpResponse(xml, content_type="text/xml; charset=utf-8") diff --git a/src/metax_api/api/rest/base/api_schemas/catalogrecord.json b/src/metax_api/api/rest/base/api_schemas/catalogrecord.json index 91a8c6ec..8e25e74f 100755 --- a/src/metax_api/api/rest/base/api_schemas/catalogrecord.json +++ b/src/metax_api/api/rest/base/api_schemas/catalogrecord.json @@ -133,12 +133,6 @@ "maxItems":1, "type":"string" }, - "editor":{ - "title":"Editor", - "description":"The editor that is used to modify the record.", - "type":"object", - "$ref":"#/definitions/Editor" - }, "dataset_version_set":{ "title":"Dataset version set", "description":"Information about the different related dataset versions.", @@ -270,36 +264,6 @@ "metadata_provider_user" ] }, - "Editor":{ - "title":"Editor", - "description":"An editor object, holds information related to an editor that is used to modify the record. Can contain other fields that are useful to the editor.", - "properties":{ - "creator_id":{ - "title":"Creator id", - "description":"Id of the creator in the editor. May (currently) be the same value as user_created in CatalogRecord, if the requestor has set that value.", - "type":"string" - }, - "owner_id":{ - "title":"Owner id", - "description":"Id of the owner in the editor", - "type":"string" - }, - "record_id":{ - "title":"Record id", - "description":"Internal id of the record in the editor's system", - "type":"string" - }, - "identifier":{ - "title":"identifier", - "description":"Id of the editor, for example: qvain", - "type":"string" - } - }, - "required":[ - "owner_id", - "identifier" - ] - }, "CatalogRecordVersion":{ "title":"CatalogRecord Version", "description":"Information about various versions of a CatalogRecord.", diff --git a/src/metax_api/api/rest/base/router.py b/src/metax_api/api/rest/base/router.py index b3bc11c2..742103a3 100755 --- a/src/metax_api/api/rest/base/router.py +++ b/src/metax_api/api/rest/base/router.py @@ -23,7 +23,6 @@ from rest_framework.routers import DefaultRouter, Route from .views import ( - ApiErrorViewSet, ContractViewSet, DataCatalogViewSet, DatasetViewSet, @@ -32,28 +31,31 @@ FileViewSet, SchemaViewSet, ) +from metax_api.api.rest.v2.views import ApiErrorViewSetV2 class CustomRouter(DefaultRouter): - def __init__(self, *args, **kwargs): """ Override to allow PUT and PATCH methods in resource list url. """ self.routes.pop(0) - self.routes.insert(0, Route( - url=r'^{prefix}{trailing_slash}$', - mapping={ - 'get': 'list', # original - 'post': 'create', # original - 'put': 'update_bulk', # custom - 'patch': 'partial_update_bulk', # custom - 'delete': 'destroy_bulk' # custom - }, - name='{basename}-list', - detail=False, - initkwargs={'suffix': 'List'} - )) + self.routes.insert( + 0, + Route( + url=r"^{prefix}{trailing_slash}$", + mapping={ + "get": "list", # original + "post": "create", # original + "put": "update_bulk", # custom + "patch": "partial_update_bulk", # custom + "delete": "destroy_bulk", # custom + }, + name="{basename}-list", + detail=False, + initkwargs={"suffix": "List"}, + ), + ) super(CustomRouter, self).__init__(*args, **kwargs) def get_default_basename(self, viewset): @@ -61,24 +63,24 @@ def get_default_basename(self, viewset): When a viewset has no queryset set, or base_name is not passed to a router as the 3rd parameter, automatically determine base name. """ - return viewset.__class__.__name__.split('View')[0] + return viewset.__class__.__name__.split("View")[0] router = CustomRouter(trailing_slash=False) -router.register(r'apierrors/?', ApiErrorViewSet) -router.register(r'contracts/?', ContractViewSet) -router.register(r'datasets/?', DatasetViewSet) -router.register(r'datacatalogs/?', DataCatalogViewSet) -router.register(r'directories/?', DirectoryViewSet) -router.register(r'files/?', FileViewSet) -router.register(r'filestorages/?', FileStorageViewSet) -router.register(r'schemas/?', SchemaViewSet) +router.register(r"apierrors/?", ApiErrorViewSetV2) +router.register(r"contracts/?", ContractViewSet) +router.register(r"datasets/?", DatasetViewSet) +router.register(r"datacatalogs/?", DataCatalogViewSet) +router.register(r"directories/?", DirectoryViewSet) +router.register(r"files/?", FileViewSet) +router.register(r"filestorages/?", FileStorageViewSet) +router.register(r"schemas/?", SchemaViewSet) # note: this somehow maps to list-api... but the end result works when # the presence of the parameters is inspected in the list-api method. router.register( - r'datasets/(?P.+)/metadata_versions/(?P.+)/?', - DatasetViewSet + r"datasets/(?P.+)/metadata_versions/(?P.+)/?", + DatasetViewSet, ) api_urlpatterns = router.urls diff --git a/src/metax_api/api/rest/base/schemas/att_dataset_schema.json b/src/metax_api/api/rest/base/schemas/att_dataset_schema.json index 5047bcbd..a0e67f41 100755 --- a/src/metax_api/api/rest/base/schemas/att_dataset_schema.json +++ b/src/metax_api/api/rest/base/schemas/att_dataset_schema.json @@ -252,14 +252,6 @@ "type":"object", "$ref":"#/definitions/CatalogRecord" }, - "editor":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#editor", - "title":"Editor", - "description":"Software or service that is used to modify the catalog record and the dataset", - "@type":"@id", - "type":"object", - "$ref":"#/definitions/Editor" - }, "identifier":{ "@id":"http://uri.suomi.fi/datamodel/ns/mrd#identifier", "title":"Identifier", @@ -465,51 +457,6 @@ ], "additionalProperties": false }, - "Editor":{ - "title":"Editor", - "type":"object", - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#Editor", - "description":"Software or service that is used to modify the catalog record and the dataset.", - "minProperties":1, - "properties":{ - "identifier":{ - "@id":"http://purl.org/dc/terms/identifier", - "title":"Identifier", - "description":"Identifier of the editor, such as Qvain or harvester.", - "enum":[ - "QVAIN" - ], - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "owner_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#owner_identifier", - "title":"Owner identifier", - "description":"owner of the resource", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "creator_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#creator_identifier", - "title":"Creator identifier", - "description":"creator of the resource", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "record_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#recordIdentifier", - "title":"Record identifier", - "description":"local record identifier", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - } - }, - "additionalProperties": false - }, "EntityRelation":{ "title":"Entity relation", "type":"object", diff --git a/src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json b/src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json index 55a099e6..cc047bcb 100755 --- a/src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json +++ b/src/metax_api/api/rest/base/schemas/harvester_dataset_schema.json @@ -253,14 +253,6 @@ "type":"object", "$ref":"#/definitions/CatalogRecord" }, - "editor":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#editor", - "title":"Editor", - "description":"Software or service that is used to modify the catalog record and the dataset", - "@type":"@id", - "type":"object", - "$ref":"#/definitions/Editor" - }, "identifier":{ "@id":"http://uri.suomi.fi/datamodel/ns/mrd#identifier", "title":"Identifier", @@ -514,51 +506,6 @@ ], "additionalProperties": false }, - "Editor":{ - "title":"Editor", - "type":"object", - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#Editor", - "description":"Software or service that is used to modify the catalog record and the dataset.", - "minProperties":1, - "properties":{ - "identifier":{ - "@id":"http://purl.org/dc/terms/identifier", - "title":"Identifier", - "description":"Identifier of the editor, such as Qvain or harvester.", - "enum":[ - "QVAIN" - ], - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "owner_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#owner_identifier", - "title":"Owner identifier", - "description":"owner of the resource", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "creator_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#creator_identifier", - "title":"Creator identifier", - "description":"creator of the resource", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "record_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#recordIdentifier", - "title":"Record identifier", - "description":"local record identifier", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - } - }, - "additionalProperties": true - }, "EntityRelation":{ "title":"Entity relation", "type":"object", diff --git a/src/metax_api/api/rest/base/schemas/ida_dataset_schema.json b/src/metax_api/api/rest/base/schemas/ida_dataset_schema.json index 810b92e1..3e59da3c 100755 --- a/src/metax_api/api/rest/base/schemas/ida_dataset_schema.json +++ b/src/metax_api/api/rest/base/schemas/ida_dataset_schema.json @@ -252,14 +252,6 @@ "type":"object", "$ref":"#/definitions/CatalogRecord" }, - "editor":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#editor", - "title":"Editor", - "description":"Software or service that is used to modify the catalog record and the dataset", - "@type":"@id", - "type":"object", - "$ref":"#/definitions/Editor" - }, "identifier":{ "@id":"http://uri.suomi.fi/datamodel/ns/mrd#identifier", "title":"Identifier", @@ -490,51 +482,6 @@ "identifier" ] }, - "Editor":{ - "title":"Editor", - "type":"object", - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#Editor", - "description":"Software or service that is used to modify the catalog record and the dataset.", - "minProperties":1, - "properties":{ - "identifier":{ - "@id":"http://purl.org/dc/terms/identifier", - "title":"Identifier", - "description":"Identifier of the editor, such as Qvain or harvester.", - "enum":[ - "QVAIN" - ], - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "owner_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#owner_identifier", - "title":"Owner identifier", - "description":"owner of the resource", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "creator_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#creator_identifier", - "title":"Creator identifier", - "description":"creator of the resource", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "record_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#recordIdentifier", - "title":"Record identifier", - "description":"local record identifier", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - } - }, - "additionalProperties": true - }, "EntityRelation":{ "title":"Entity relation", "type":"object", diff --git a/src/metax_api/api/rest/base/serializers/__init__.py b/src/metax_api/api/rest/base/serializers/__init__.py index 68d4c8fc..1c243720 100755 --- a/src/metax_api/api/rest/base/serializers/__init__.py +++ b/src/metax_api/api/rest/base/serializers/__init__.py @@ -12,4 +12,4 @@ from .file_serializer import FileSerializer, LightFileSerializer from .file_storage_serializer import FileStorageSerializer from .serializer_utils import validate_json -from .xml_metadata_serializer import XmlMetadataSerializer \ No newline at end of file +from .xml_metadata_serializer import XmlMetadataSerializer diff --git a/src/metax_api/api/rest/base/serializers/catalog_record_serializer.py b/src/metax_api/api/rest/base/serializers/catalog_record_serializer.py index 99428620..c262c020 100755 --- a/src/metax_api/api/rest/base/serializers/catalog_record_serializer.py +++ b/src/metax_api/api/rest/base/serializers/catalog_record_serializer.py @@ -29,85 +29,82 @@ # when end user creates a record, strip all fields except these END_USER_CREATE_ALLOWED_FIELDS = [ - 'data_catalog', - 'research_dataset', - + "data_catalog", + "research_dataset", # not set by the user, but are set by metax, so should not be discarded - 'date_created', - 'user_created', - '__request' + "date_created", + "user_created", + "__request", ] # when end user updates a record, strip all fields except these END_USER_UPDATE_ALLOWED_FIELDS = [ - 'research_dataset', - + "research_dataset", # not set by the user, but are set by metax, so should not be discarded - 'date_modified', - 'user_modified', - 'service_modified', - '__request' + "date_modified", + "user_modified", + "service_modified", + "__request", ] END_USER_ALLOWED_DATA_CATALOGS = django_settings.END_USER_ALLOWED_DATA_CATALOGS LEGACY_CATALOGS = django_settings.LEGACY_CATALOGS DFT_CATALOG = django_settings.DFT_DATA_CATALOG_IDENTIFIER -class CatalogRecordSerializer(CommonSerializer): +class CatalogRecordSerializer(CommonSerializer): class Meta: fields = ( - 'id', - 'identifier', - 'alternate_record_set', - 'contract', - 'data_catalog', - 'dataset_version_set', - 'deprecated', - 'date_deprecated', - 'metadata_owner_org', - 'metadata_provider_org', - 'metadata_provider_user', - 'research_dataset', - 'preservation_dataset_version', - 'preservation_dataset_origin_version', - 'preservation_state', - 'preservation_state_modified', - 'preservation_description', - 'preservation_reason_description', - 'preservation_identifier', - 'next_dataset_version', - 'previous_dataset_version', - 'mets_object_identifier', - 'state', - 'use_doi_for_published', - 'editor', - 'cumulative_state', - 'date_cumulation_started', - 'date_cumulation_ended', - 'date_last_cumulative_addition', - 'rems_identifier', - 'access_granter', - 'api_meta' + "id", + "identifier", + "alternate_record_set", + "contract", + "data_catalog", + "dataset_version_set", + "deprecated", + "date_deprecated", + "metadata_owner_org", + "metadata_provider_org", + "metadata_provider_user", + "research_dataset", + "preservation_dataset_version", + "preservation_dataset_origin_version", + "preservation_state", + "preservation_state_modified", + "preservation_description", + "preservation_reason_description", + "preservation_identifier", + "next_dataset_version", + "previous_dataset_version", + "mets_object_identifier", + "state", + "use_doi_for_published", + "cumulative_state", + "date_cumulation_started", + "date_cumulation_ended", + "date_last_cumulative_addition", + "rems_identifier", + "access_granter", + "api_meta", ) + CommonSerializer.Meta.fields extra_kwargs = { # these values are generated automatically or provide default values on creation. # some fields can be later updated by the user, some are generated - 'identifier': { 'required': False }, - 'preservation_state': { 'required': False }, - 'preservation_description': { 'required': False }, - 'preservation_state_modified': { 'required': False }, - 'mets_object_identifier': { 'required': False }, - 'next_dataset_version': { 'required': False }, - 'previous_dataset_version': { 'required': False }, - 'preservation_dataset_origin_version': { 'required': False }, + "identifier": {"required": False}, + "preservation_state": {"required": False}, + "preservation_description": {"required": False}, + "preservation_state_modified": {"required": False}, + "mets_object_identifier": {"required": False}, + "next_dataset_version": {"required": False}, + "previous_dataset_version": {"required": False}, + "preservation_dataset_origin_version": {"required": False}, } extra_kwargs.update(CommonSerializer.Meta.extra_kwargs) # schemas dir is effectively ../schemas/ - _schemas_directory_path = path.join(path.dirname(path.dirname(__file__)), 'schemas') + _schemas_directory_path = path.join(path.dirname(path.dirname(__file__)), "schemas") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -120,23 +117,26 @@ def is_valid(self, raise_exception=False): elif self._operation_is_update: self._end_user_update_validations(self.instance, self.initial_data) - if self.initial_data.get('data_catalog', False): - self.initial_data['data_catalog'] = self._get_id_from_related_object( - 'data_catalog', self._get_data_catalog_relation) - if self.initial_data.get('contract', False): - self.initial_data['contract'] = self._get_id_from_related_object('contract', self._get_contract_relation) - - self.initial_data.pop('alternate_record_set', None) - self.initial_data.pop('dataset_version_set', None) - self.initial_data.pop('next_dataset_version', None) - self.initial_data.pop('previous_dataset_version', None) - self.initial_data.pop('deprecated', None) - self.initial_data.pop('date_deprecated', None) - self.initial_data.pop('state', None) - self.initial_data.pop('preservation_identifier', None) - self.initial_data.pop('preservation_dataset_version', None) - self.initial_data.pop('preservation_dataset_origin_version', None) - self.initial_data.pop('rems_identifier', None) + if self.initial_data.get("data_catalog", False): + self.initial_data["data_catalog"] = self._get_id_from_related_object( + "data_catalog", self._get_data_catalog_relation + ) + if self.initial_data.get("contract", False): + self.initial_data["contract"] = self._get_id_from_related_object( + "contract", self._get_contract_relation + ) + + self.initial_data.pop("alternate_record_set", None) + self.initial_data.pop("dataset_version_set", None) + self.initial_data.pop("next_dataset_version", None) + self.initial_data.pop("previous_dataset_version", None) + self.initial_data.pop("deprecated", None) + self.initial_data.pop("date_deprecated", None) + self.initial_data.pop("state", None) + self.initial_data.pop("preservation_identifier", None) + self.initial_data.pop("preservation_dataset_version", None) + self.initial_data.pop("preservation_dataset_origin_version", None) + self.initial_data.pop("rems_identifier", None) if self._data_catalog_is_changed(): # updating data catalog, but not necessarily research_dataset. @@ -152,31 +152,35 @@ def is_valid(self, raise_exception=False): # ensure any operation made on research_dataset during serializer.is_valid(), # is still compatible with the schema - if 'research_dataset' in self.initial_data: - self.validate_json_schema(self.initial_data['research_dataset']) - self._validate_org_name_is_set(self.initial_data['research_dataset']) + if "research_dataset" in self.initial_data: + self.validate_json_schema(self.initial_data["research_dataset"]) + self._validate_org_name_is_set(self.initial_data["research_dataset"]) def update(self, instance, validated_data): - if 'preserve_version' in self.context['request'].query_params: + if "preserve_version" in self.context["request"].query_params: if self._request_by_end_user(): - raise Http403({ 'detail': [ 'Parameter preserve_version not permitted for end users' ]}) + raise Http403( + {"detail": ["Parameter preserve_version not permitted for end users"]} + ) # execute updates without creating new versions instance.preserve_version = True return super(CatalogRecordSerializer, self).update(instance, validated_data) def create(self, validated_data): - if 'V2' not in self.__class__.__name__ and \ - validated_data['data_catalog'].catalog_json['identifier'] == DFT_CATALOG: - raise Http400({ 'detail': [ 'Draft catalog cannot be used in V1 API' ]}) + if ( + "V2" not in self.__class__.__name__ + and validated_data["data_catalog"].catalog_json["identifier"] == DFT_CATALOG + ): + raise Http400({"detail": ["Draft catalog cannot be used in V1 API"]}) if self._migration_override_requested(): # any custom stuff before create that might be necessary for migration purposes - pid = '' - if validated_data['research_dataset'].get('preferred_identifier', False): + pid = "" + if validated_data["research_dataset"].get("preferred_identifier", False): # store pid, since it will be overwritten during create otherwise - pid = validated_data['research_dataset']['preferred_identifier'] + pid = validated_data["research_dataset"]["preferred_identifier"] res = super().create(validated_data) @@ -185,7 +189,7 @@ def create(self, validated_data): # any custom stuff after create that my be necessary for migration purposes if pid: # save original pid provided by the requestor - res.research_dataset['preferred_identifier'] = pid + res.research_dataset["preferred_identifier"] = pid # save, while bypassing normal save-related procedures in CatalogRecord model super(Common, res).save() @@ -196,8 +200,10 @@ def _end_user_update_validations(self, instance, validated_data): """ Enforce some rules related to end users when updating records. """ - self._check_end_user_allowed_catalogs(instance.data_catalog.catalog_json['identifier']) - fields_to_discard = [ key for key in validated_data.keys() if key not in END_USER_UPDATE_ALLOWED_FIELDS ] + self._check_end_user_allowed_catalogs(instance.data_catalog.catalog_json["identifier"]) + fields_to_discard = [ + key for key in validated_data.keys() if key not in END_USER_UPDATE_ALLOWED_FIELDS + ] for field_name in fields_to_discard: del validated_data[field_name] @@ -208,23 +214,29 @@ def _end_user_create_validations(self, validated_data): - Can only create records into specified catalogs (and can only use the identifier value, not id's directly!) - Will have some fields automatically filled for them """ - fields_to_discard = [ key for key in validated_data.keys() if key not in END_USER_CREATE_ALLOWED_FIELDS ] + fields_to_discard = [ + key for key in validated_data.keys() if key not in END_USER_CREATE_ALLOWED_FIELDS + ] for field_name in fields_to_discard: del validated_data[field_name] # set some fields to whoever the authentication token belonged to. - validated_data['metadata_provider_user'] = self.context['request'].user.username - validated_data['metadata_provider_org'] = self.context['request'].user.token['schacHomeOrganization'] - validated_data['metadata_owner_org'] = self.context['request'].user.token['schacHomeOrganization'] + validated_data["metadata_provider_user"] = self.context["request"].user.username + validated_data["metadata_provider_org"] = self.context["request"].user.token[ + "schacHomeOrganization" + ] + validated_data["metadata_owner_org"] = self.context["request"].user.token[ + "schacHomeOrganization" + ] try: - dc_identifier = validated_data['data_catalog'].catalog_json['identifier'] + dc_identifier = validated_data["data_catalog"].catalog_json["identifier"] except: try: - if isinstance(validated_data['data_catalog'], dict): - dc_identifier = validated_data['data_catalog']['identifier'] + if isinstance(validated_data["data_catalog"], dict): + dc_identifier = validated_data["data_catalog"]["identifier"] else: - dc_identifier = validated_data['data_catalog'] + dc_identifier = validated_data["data_catalog"] except KeyError: # an error is raise later about missing required field return @@ -233,74 +245,82 @@ def _end_user_create_validations(self, validated_data): def _check_end_user_allowed_catalogs(self, dc_identifier): if dc_identifier not in END_USER_ALLOWED_DATA_CATALOGS: - raise Http403({ - 'detail': [ - 'You do not have access to the selected data catalog. Please use one of the following ' - 'catalogs: %s' % ', '.join(END_USER_ALLOWED_DATA_CATALOGS) - ] - }) + raise Http403( + { + "detail": [ + "You do not have access to the selected data catalog. Please use one of the following " + "catalogs: %s" % ", ".join(END_USER_ALLOWED_DATA_CATALOGS) + ] + } + ) def to_representation(self, instance): res = super(CatalogRecordSerializer, self).to_representation(instance) - if 'data_catalog' in res: - if self.expand_relation_requested('data_catalog'): - res['data_catalog'] = DataCatalogSerializer(instance.data_catalog).data + if "data_catalog" in res: + if self.expand_relation_requested("data_catalog"): + res["data_catalog"] = DataCatalogSerializer(instance.data_catalog).data else: - res['data_catalog'] = { - 'id': instance.data_catalog.id, - 'identifier': instance.data_catalog.catalog_json['identifier'], + res["data_catalog"] = { + "id": instance.data_catalog.id, + "identifier": instance.data_catalog.catalog_json["identifier"], } - if 'contract' in res: - if self.expand_relation_requested('contract'): - res['contract'] = ContractSerializer(instance.contract).data + if "contract" in res: + if self.expand_relation_requested("contract"): + res["contract"] = ContractSerializer(instance.contract).data else: - res['contract'] = { - 'id': instance.contract.id, - 'identifier': instance.contract.contract_json['identifier'] + res["contract"] = { + "id": instance.contract.id, + "identifier": instance.contract.contract_json["identifier"], } - if 'alternate_record_set' in res: + if "alternate_record_set" in res: alternate_records = instance.alternate_record_set.records.exclude(pk=instance.id) if len(alternate_records): - res['alternate_record_set'] = [ ar.identifier for ar in alternate_records ] + res["alternate_record_set"] = [ar.identifier for ar in alternate_records] - if 'dataset_version_set' in res: - res['dataset_version_set'] = instance.dataset_version_set.get_listing() + if "dataset_version_set" in res: + res["dataset_version_set"] = instance.dataset_version_set.get_listing() - if 'next_dataset_version' in res: + if "next_dataset_version" in res: if instance.next_dataset_version.state == CatalogRecord.STATE_PUBLISHED: - res['next_dataset_version'] = instance.next_dataset_version.identifiers_dict - elif instance.user_is_privileged(instance.request or self.context['request']): + res["next_dataset_version"] = instance.next_dataset_version.identifiers_dict + elif instance.user_is_privileged(instance.request or self.context["request"]): # include additional information to show the owner this version is actually still just a draft - res['next_dataset_version'] = instance.next_dataset_version.identifiers_dict - res['next_dataset_version']['state'] = CatalogRecord.STATE_DRAFT + res["next_dataset_version"] = instance.next_dataset_version.identifiers_dict + res["next_dataset_version"]["state"] = CatalogRecord.STATE_DRAFT else: # if the next dataset version is still just a draft, then unauthorized users dont need to know about it. - del res['next_dataset_version'] - - if 'previous_dataset_version' in res: - res['previous_dataset_version'] = instance.previous_dataset_version.identifiers_dict - - if 'preservation_dataset_version' in res: - res['preservation_dataset_version'] = instance.preservation_dataset_version.identifiers_dict - res['preservation_dataset_version']['preservation_state'] = \ - instance.preservation_dataset_version.preservation_state - - elif 'preservation_dataset_origin_version' in res: - res['preservation_dataset_origin_version'] = instance.preservation_dataset_origin_version.identifiers_dict - res['preservation_dataset_origin_version']['deprecated'] = \ - instance.preservation_dataset_origin_version.deprecated + del res["next_dataset_version"] + + if "previous_dataset_version" in res: + res["previous_dataset_version"] = instance.previous_dataset_version.identifiers_dict + + if "preservation_dataset_version" in res: + res[ + "preservation_dataset_version" + ] = instance.preservation_dataset_version.identifiers_dict + res["preservation_dataset_version"][ + "preservation_state" + ] = instance.preservation_dataset_version.preservation_state + + elif "preservation_dataset_origin_version" in res: + res[ + "preservation_dataset_origin_version" + ] = instance.preservation_dataset_origin_version.identifiers_dict + res["preservation_dataset_origin_version"][ + "deprecated" + ] = instance.preservation_dataset_origin_version.deprecated if instance.new_dataset_version_created: - res['new_version_created'] = instance.new_dataset_version_created + res["new_version_created"] = instance.new_dataset_version_created # Do the population of file_details here, since if it was done in the view, it might not know the file/dir # identifiers any longer, since the potential stripping of file/dir fields takes away identifier fields from # File and Directory objects, which are needed in populating file_details - if 'request' in self.context and 'file_details' in self.context['request'].query_params: - CRS.populate_file_details(res, self.context['request']) + if "request" in self.context and "file_details" in self.context["request"].query_params: + CRS.populate_file_details(res, self.context["request"]) res = self._check_and_strip_sensitive_fields(instance, res) @@ -324,15 +344,16 @@ def _check_and_strip_sensitive_fields(self, instance, res): retrieving a list where some filter was used, the identifiers may not be known either... It is much more straightforward to do it here. """ - if 'request' in self.context: - if not instance.user_is_privileged(self.context['request']): - if res.get('research_dataset'): + if "request" in self.context: + if not instance.user_is_privileged(self.context["request"]): + if res.get("research_dataset"): # research_dataset can be missing if not listed in fields query param - res['research_dataset'] = CRS.check_and_remove_metadata_based_on_access_type( - CRS.remove_contact_info_metadata(res['research_dataset'])) + res["research_dataset"] = CRS.check_and_remove_metadata_based_on_access_type( + CRS.remove_contact_info_metadata(res["research_dataset"]) + ) - res.pop('rems_identifier', None) - res.pop('access_granter', None) + res.pop("rems_identifier", None) + res.pop("access_granter", None) return res @@ -340,29 +361,32 @@ def _populate_dir_titles(self, ds): """ If dir title has been omitted, populate it with its dir name. """ - if 'directories' not in ds: + if "directories" not in ds: return # make sure to not populate title for entries that already contain other dataset-specific metadata dirs_to_populate = [ - dr['identifier'] for dr in ds['directories'] - if dr.get('title', None) is None + dr["identifier"] + for dr in ds["directories"] + if dr.get("title", None) is None and len(dr) > 1 - and dr.get('exclude', False) is False - and dr.get('delete', False) is False + and dr.get("exclude", False) is False + and dr.get("delete", False) is False ] if dirs_to_populate: dirs_from_db = [ - dr for dr in - Directory.objects.filter(identifier__in=dirs_to_populate).values('identifier', 'directory_name') + dr + for dr in Directory.objects.filter(identifier__in=dirs_to_populate).values( + "identifier", "directory_name" + ) ] - for dr in ds['directories']: + for dr in ds["directories"]: for i, dir_details in enumerate(dirs_from_db): - if dir_details['identifier'] == dr['identifier']: - dr['title'] = dir_details['directory_name'] + if dir_details["identifier"] == dr["identifier"]: + dr["title"] = dir_details["directory_name"] dirs_from_db.pop(i) break @@ -370,38 +394,43 @@ def _populate_file_titles(self, ds): """ If file title has been omitted, populate it with its file name. """ - if 'files' not in ds: + if "files" not in ds: return # make sure to not populate title for entries that already contain other dataset-specific metadata files_to_populate = [ - f['identifier'] for f in ds['files'] - if f.get('title', None) is None + f["identifier"] + for f in ds["files"] + if f.get("title", None) is None and len(f) > 1 - and f.get('exclude', False) is False - and f.get('delete', False) is False + and f.get("exclude", False) is False + and f.get("delete", False) is False ] if files_to_populate: files_from_db = [ - f for f in - File.objects.filter(identifier__in=files_to_populate).values('identifier', 'file_name') + f + for f in File.objects.filter(identifier__in=files_to_populate).values( + "identifier", "file_name" + ) ] - for f in ds['files']: + for f in ds["files"]: for i, file_details in enumerate(files_from_db): - if file_details['identifier'] == f['identifier']: - f['title'] = file_details['file_name'] + if file_details["identifier"] == f["identifier"]: + f["title"] = file_details["file_name"] files_from_db.pop(i) break def _validate_draft_data_catalog(self): # catalog object is not yet included to initial_data so have to fetch it - dc_pid = DataCatalog.objects.get(pk=self.initial_data['data_catalog']).catalog_json['identifier'] + dc_pid = DataCatalog.objects.get(pk=self.initial_data["data_catalog"]).catalog_json[ + "identifier" + ] if dc_pid == DFT_CATALOG and (self.instance.is_published() or self.instance.draft_of): - raise ValidationError({ 'detail': ['Catalog cannot be changed back to draft'] }) + raise ValidationError({"detail": ["Catalog cannot be changed back to draft"]}) def validate_research_dataset(self, value): self._populate_file_and_dir_titles(value) @@ -416,16 +445,16 @@ def validate_research_dataset(self, value): return value def _populate_file_and_dir_titles(self, value): - if 'directories' in value and not value['directories']: + if "directories" in value and not value["directories"]: # remove if empty list - del value['directories'] + del value["directories"] else: self._populate_dir_titles(value) - if 'files' in value and not value['files']: + if "files" in value and not value["files"]: # remove if empty list - del value['files'] + del value["files"] else: self._populate_file_titles(value) @@ -433,34 +462,48 @@ def validate_json_schema(self, value): self._set_dataset_schema() if self._operation_is_create: - if not value.get('preferred_identifier', None): - if DataCatalogService.is_harvested(self.initial_data.get('data_catalog')): - raise ValidationError({ 'preferred_identifier': - ['harvested catalog record must have preferred identifier']}) + if not value.get("preferred_identifier", None): + if DataCatalogService.is_harvested(self.initial_data.get("data_catalog")): + raise ValidationError( + { + "preferred_identifier": [ + "harvested catalog record must have preferred identifier" + ] + } + ) # normally not present, but may be set by harvesters. if missing, # use temporary value and remove after schema validation. - value['preferred_identifier'] = 'temp' + value["preferred_identifier"] = "temp" if self._migration_override_requested(): - for is_output_of in value.get('is_output_of', []): - if 'source_organization' not in is_output_of or not is_output_of['source_organization']: - is_output_of['source_organization'] = [ - {'@type': 'Organization', - 'identifier': 'MIGRATION_OVERRIDE', - 'name': {'und': 'temp'}} + for is_output_of in value.get("is_output_of", []): + if ( + "source_organization" not in is_output_of + or not is_output_of["source_organization"] + ): + is_output_of["source_organization"] = [ + { + "@type": "Organization", + "identifier": "MIGRATION_OVERRIDE", + "name": {"und": "temp"}, + } ] validate_json(value, self.json_schema) - if value['preferred_identifier'] == 'temp': - value.pop('preferred_identifier') + if value["preferred_identifier"] == "temp": + value.pop("preferred_identifier") if self._migration_override_requested(): - for is_output_of in value.get('is_output_of', []): - if 'source_organization' in is_output_of and len(is_output_of['source_organization']) == 1 and \ - is_output_of['source_organization'][0]['identifier'] == 'MIGRATION_OVERRIDE': - is_output_of.pop('source_organization', None) + for is_output_of in value.get("is_output_of", []): + if ( + "source_organization" in is_output_of + and len(is_output_of["source_organization"]) == 1 + and is_output_of["source_organization"][0]["identifier"] + == "MIGRATION_OVERRIDE" + ): + is_output_of.pop("source_organization", None) else: # update operations @@ -476,16 +519,18 @@ def _validate_org_name_is_set(self, obj): a name. """ if isinstance(obj, dict): - if '@type' in obj and obj['@type'] == 'Organization' and 'name' not in obj: - raise ValidationError({ - 'detail': [ - 'Specified organization object does not have a name. If you are using ' - 'an org identifier from reference data, then the name will be populated ' - 'automatically. If your org identifier is not from reference data, you ' - 'must provide the organization name. The object that caused the error: %s' - % str(obj) - ] - }) + if "@type" in obj and obj["@type"] == "Organization" and "name" not in obj: + raise ValidationError( + { + "detail": [ + "Specified organization object does not have a name. If you are using " + "an org identifier from reference data, then the name will be populated " + "automatically. If your org identifier is not from reference data, you " + "must provide the organization name. The object that caused the error: %s" + % str(obj) + ] + } + ) for field, value in obj.items(): if isinstance(value, (dict, list)): self._validate_org_name_is_set(value) @@ -514,34 +559,42 @@ def _validate_research_dataset_uniqueness(self, research_dataset): if not self._catalog_enforces_unique_pids(): return - preferred_identifier_value = research_dataset.get('preferred_identifier', None) + preferred_identifier_value = research_dataset.get("preferred_identifier", None) if not preferred_identifier_value: # during create, preferred_identifier is not necessarily set return - if self._find_object_using_identifier('preferred_identifier', preferred_identifier_value): + if self._find_object_using_identifier("preferred_identifier", preferred_identifier_value): if self._data_catalog_supports_versioning(): - raise ValidationError([ - 'A catalog record with this research_dataset ->> preferred_identifier' - ' already exists in another data catalog. When saving to ATT catalog,' - ' the preferred_identifier must not already exist in other catalogs.' - ]) + raise ValidationError( + [ + "A catalog record with this research_dataset ->> preferred_identifier" + " already exists in another data catalog. When saving to ATT catalog," + " the preferred_identifier must not already exist in other catalogs." + ] + ) else: # harvested catalog - raise ValidationError([ - 'A catalog record with this research_dataset ->> preferred_identifier' - ' already exists in this data catalog.' - ]) + raise ValidationError( + [ + "A catalog record with this research_dataset ->> preferred_identifier" + " already exists in this data catalog." + ] + ) # cr not found using preferred_identifier. preferred_identifier value should never # be the same as metadata_version_identifier value in any catalog, so look for existing records # using metadata_version_identifier also - if self._find_object_using_identifier('metadata_version_identifier', preferred_identifier_value): - raise ValidationError([ - 'A catalog record already exists which has the given preferred_identifier' - ' value as its metadata_version_identifier value.' - ]) + if self._find_object_using_identifier( + "metadata_version_identifier", preferred_identifier_value + ): + raise ValidationError( + [ + "A catalog record already exists which has the given preferred_identifier" + " value as its metadata_version_identifier value." + ] + ) def _catalog_enforces_unique_pids(self): """ @@ -550,15 +603,17 @@ def _catalog_enforces_unique_pids(self): """ if self._operation_is_create: try: - dc = DataCatalog.objects.values('catalog_json').get(pk=self.initial_data['data_catalog']) + dc = DataCatalog.objects.values("catalog_json").get( + pk=self.initial_data["data_catalog"] + ) except DataCatalog.DoesNotExist: - raise ValidationError({ 'detail': ['Provided data catalog does not exist']}) + raise ValidationError({"detail": ["Provided data catalog does not exist"]}) except KeyError: # data_catalog was omitted. an approriate error is raised later. return - dc_pid = dc['catalog_json']['identifier'] + dc_pid = dc["catalog_json"]["identifier"] else: - dc_pid = self.instance.data_catalog.catalog_json['identifier'] + dc_pid = self.instance.data_catalog.catalog_json["identifier"] return dc_pid not in LEGACY_CATALOGS @@ -569,28 +624,28 @@ def _find_object_using_identifier(self, field_name, identifier): - takes into account data_catalog when searching by preferred_identifier - does not use select_related() to also fetch relations, since they are not needed. """ - params = { 'research_dataset__contains': { field_name: identifier }} + params = {"research_dataset__contains": {field_name: identifier}} - if field_name == 'preferred_identifier' and not self._data_catalog_supports_versioning(): + if field_name == "preferred_identifier" and not self._data_catalog_supports_versioning(): # only look for hits within the same data catalog. if self._operation_is_create: # value of data_catalog in initial_data is set in is_valid() - params['data_catalog'] = self.initial_data['data_catalog'] + params["data_catalog"] = self.initial_data["data_catalog"] else: # updates - if 'data_catalog' in self.initial_data: + if "data_catalog" in self.initial_data: # the update operation is updating data_catalog as well, # so make sure the new catalog is checked for not having # the identifier currently being checked. # value of data_catalog in initial_data is set in is_valid() - params['data_catalog'] = self.initial_data['data_catalog'] + params["data_catalog"] = self.initial_data["data_catalog"] else: # a PATCH which does not contain data_catalog - get # data_catalog id from the instance being updated == what # is currently in db - params['data_catalog'] = self.instance.data_catalog.id + params["data_catalog"] = self.instance.data_catalog.id else: # checking metadata_version_identifier, or saving to ATT catalog - in both cases, find matches @@ -603,88 +658,107 @@ def _find_object_using_identifier(self, field_name, identifier): # preferred_identifiers already existing in ATT catalogs are fine, so exclude # results from ATT catalogs. matches in other catalogs however are considered # an error. - return CatalogRecord.objects_unfiltered.filter(**params) \ - .exclude(data_catalog_id=self.instance.data_catalog_id).exists() + return ( + CatalogRecord.objects_unfiltered.filter(**params) + .exclude(data_catalog_id=self.instance.data_catalog_id) + .exists() + ) else: - return CatalogRecord.objects_unfiltered.filter(**params).exclude(pk=self.instance.id).exists() + return ( + CatalogRecord.objects_unfiltered.filter(**params) + .exclude(pk=self.instance.id) + .exists() + ) def _data_catalog_is_changed(self): """ Check if data_catalog of the record is being changed. Used to decide if preferred_identifier uniqueness should be checked in certain situations. """ - if self._operation_is_update and 'data_catalog' in self.initial_data: - dc = self.initial_data['data_catalog'] + if self._operation_is_update and "data_catalog" in self.initial_data: + dc = self.initial_data["data_catalog"] if isinstance(dc, int): return dc != self.instance.data_catalog.id elif isinstance(dc, str): - return dc != self.instance.catalog_json['identifier'] + return dc != self.instance.catalog_json["identifier"] elif isinstance(dc, dict): - return dc['identifier'] != self.instance.catalog_json['identifier'] - else: # pragma: no cover - raise ValidationError({ 'detail': ['can not figure out the type of data_catalog'] }) + return dc["identifier"] != self.instance.catalog_json["identifier"] + else: # pragma: no cover + raise ValidationError({"detail": ["can not figure out the type of data_catalog"]}) def _preferred_identifier_is_changed(self): """ Check if preferred_identifier is being updated in the current request or not. """ - return self.initial_data['research_dataset'].get('preferred_identifier', None) \ + return ( + self.initial_data["research_dataset"].get("preferred_identifier", None) != self.instance.preferred_identifier + ) def _data_catalog_supports_versioning(self): - if 'data_catalog' in self.initial_data: + if "data_catalog" in self.initial_data: # must always fetch from db, to know if it supports versioning or not - catalog_json = DataCatalog.objects.filter(pk=self.initial_data['data_catalog']) \ - .only('catalog_json').first().catalog_json + catalog_json = ( + DataCatalog.objects.filter(pk=self.initial_data["data_catalog"]) + .only("catalog_json") + .first() + .catalog_json + ) else: try: catalog_json = self.instance.data_catalog.catalog_json except AttributeError: - raise ValidationError({ 'data_catalog': ['data_catalog is a required field']}) + raise ValidationError({"data_catalog": ["data_catalog is a required field"]}) - return catalog_json.get('dataset_versioning', False) is True + return catalog_json.get("dataset_versioning", False) is True def _get_contract_relation(self, identifier_value): """ Passed to _get_id_from_related_object() to be used when relation was a string identifier """ if isinstance(identifier_value, dict): - identifier_value = identifier_value['contract_json']['identifier'] + identifier_value = identifier_value["contract_json"]["identifier"] try: - return Contract.objects.get( - contract_json__contains={ 'identifier': identifier_value } - ).id + return Contract.objects.get(contract_json__contains={"identifier": identifier_value}).id except Contract.DoesNotExist: - raise ValidationError({ 'contract': ['identifier %s not found.' % str(identifier_value)]}) + raise ValidationError( + {"contract": ["identifier %s not found." % str(identifier_value)]} + ) def _get_data_catalog_relation(self, identifier_value): """ Passed to _get_id_from_related_object() to be used when relation was a string identifier """ if isinstance(identifier_value, dict): - identifier_value = identifier_value['catalog_json']['identifier'] + identifier_value = identifier_value["catalog_json"]["identifier"] try: return DataCatalog.objects.get( - catalog_json__contains={ 'identifier': identifier_value } + catalog_json__contains={"identifier": identifier_value} ).id except DataCatalog.DoesNotExist: - raise ValidationError({ 'data_catalog': ['identifier %s not found' % str(identifier_value)]}) + raise ValidationError( + {"data_catalog": ["identifier %s not found" % str(identifier_value)]} + ) def _migration_override_requested(self): """ Check presence of query parameter ?migration_override, which enables some specific actions during the request, at this point useful only for migration operations. """ - migration_override = CommonService.get_boolean_query_param(self.context['request'], 'migration_override') + migration_override = CommonService.get_boolean_query_param( + self.context["request"], "migration_override" + ) if migration_override and self._request_by_end_user(): - raise Http403({ 'detail': [ 'Parameter migration_override not permitted for end users' ]}) + raise Http403({"detail": ["Parameter migration_override not permitted for end users"]}) return migration_override def _set_dataset_schema(self): data_catalog = None if self._operation_is_create: try: - data_catalog_id = self._get_id_from_related_object('data_catalog', self._get_data_catalog_relation) + data_catalog_id = self._get_id_from_related_object( + "data_catalog", self._get_data_catalog_relation + ) data_catalog = DataCatalog.objects.get(pk=data_catalog_id) except: # whatever error happened with data catalog handling - invalid data_catalog @@ -696,8 +770,10 @@ def _set_dataset_schema(self): data_catalog = self.instance.data_catalog if data_catalog: - schema_prefix = data_catalog.catalog_json.get('research_dataset_schema', None) + schema_prefix = data_catalog.catalog_json.get("research_dataset_schema", None) else: schema_prefix = None - self.json_schema = CommonService.get_json_schema(self._schemas_directory_path, 'dataset', schema_prefix) + self.json_schema = CommonService.get_json_schema( + self._schemas_directory_path, "dataset", schema_prefix + ) diff --git a/src/metax_api/api/rest/base/serializers/common_serializer.py b/src/metax_api/api/rest/base/serializers/common_serializer.py index 09ee5535..d44fe618 100755 --- a/src/metax_api/api/rest/base/serializers/common_serializer.py +++ b/src/metax_api/api/rest/base/serializers/common_serializer.py @@ -30,26 +30,26 @@ class CommonSerializer(ModelSerializer): class Meta: model = Common fields = ( - 'user_modified', - 'date_modified', - 'user_created', - 'date_created', - 'service_modified', - 'service_created', - 'removed', - 'date_removed' + "user_modified", + "date_modified", + "user_created", + "date_created", + "service_modified", + "service_created", + "removed", + "date_removed", ) extra_kwargs = { # not required during creation, or updating # they would be overwritten by the api anyway. # except for user_modified can and should # be given by the requestor if possible. - 'user_modified': { 'required': False }, - 'date_modified': { 'required': False }, - 'user_created': { 'required': False }, - 'date_created': { 'required': False }, - 'service_modified': { 'required': False }, - 'service_created': { 'required': False }, + "user_modified": {"required": False}, + "date_modified": {"required": False}, + "user_created": {"required": False}, + "date_created": {"required": False}, + "service_modified": {"required": False}, + "service_created": {"required": False}, } _operation_is_update = False @@ -68,11 +68,11 @@ def __init__(self, *args, **kwargs): super(CommonSerializer, self).__init__(*args, **kwargs) - if hasattr(self, 'initial_data') and self.initial_data is not None: - self.initial_data.pop('removed', None) - self.initial_data.pop('date_removed', None) + if hasattr(self, "initial_data") and self.initial_data is not None: + self.initial_data.pop("removed", None) + self.initial_data.pop("date_removed", None) - if hasattr(self, 'instance') and self.instance is not None: + if hasattr(self, "instance") and self.instance is not None: self._operation_is_update = True else: self._operation_is_create = True @@ -89,11 +89,11 @@ def __init__(self, *args, **kwargs): # to do so. solution: read the docs and be aware of it. self.partial = True - if 'only_fields' in kwargs: - self.requested_fields = kwargs.pop('only_fields') + if "only_fields" in kwargs: + self.requested_fields = kwargs.pop("only_fields") - elif 'request' in self.context and 'fields' in self.context['request'].query_params: - self.requested_fields = self.context['view'].fields + elif "request" in self.context and "fields" in self.context["request"].query_params: + self.requested_fields = self.context["view"].fields @transaction.atomic def save(self, *args, **kwargs): @@ -114,12 +114,12 @@ def save(self, *args, **kwargs): with metadata_version_identifier generation, file changes handling, alternate_record_set and versions handling. """ - if hasattr(self, 'instance') and self.instance is not None: + if hasattr(self, "instance") and self.instance is not None: # update operation. # request cant be passed as __request inside kwargs (as is done # when creating records), due to some 'known fields only' validations # along the way... this seems to be most convenient. - self.instance.request = self.context.get('request', None) + self.instance.request = self.context.get("request", None) super().save(*args, **kwargs) def create(self, validated_data): @@ -127,7 +127,7 @@ def create(self, validated_data): # the request object needs to be passed along the general parameters. # luckily, the validate_data is just passed to the Model __init__ # as **validated_data, and all our extra kwargs can ride along. - validated_data['__request'] = self.context.get('request', None) + validated_data["__request"] = self.context.get("request", None) return super().create(validated_data) def to_representation(self, instance): @@ -172,12 +172,15 @@ def expand_relation_requested(self, relation_name): to decide whether or not to include the complete relation object in the API response or not. """ - if 'view' in self.context and 'expand_relation' in self.context['view'].request.query_params: - return relation_name in self.context['view'].request.query_params['expand_relation'] + if ( + "view" in self.context + and "expand_relation" in self.context["view"].request.query_params + ): + return relation_name in self.context["view"].request.query_params["expand_relation"] return False def _get_id_from_related_object(self, relation_field, string_relation_func): - ''' + """ Use for finding out a related object's id, which Django needs to save the relation to the database. The related object, or its id, or identifier should be present in the initial data's relation field. @@ -186,7 +189,7 @@ def _get_id_from_related_object(self, relation_field, string_relation_func): :return: id of the related object :string_relation_func: a function which will be called to retrieve the related object in case the relation is a string identifier. - ''' + """ identifier_value = self.initial_data[relation_field] if isinstance(identifier_value, int): @@ -204,30 +207,45 @@ def _get_id_from_related_object(self, relation_field, string_relation_func): # the actual related object as a dict. it is expected to be # in un-tampered form with normal fields present, since # relation fields can not be updated through another object - if 'id' in identifier_value: + if "id" in identifier_value: try: - return int(identifier_value['id']) + return int(identifier_value["id"]) except: - raise ValidationError({relation_field: ['Validation error for relation id field. ' - 'Data in unexpected format']}) + raise ValidationError( + { + relation_field: [ + "Validation error for relation id field. " + "Data in unexpected format" + ] + } + ) else: # try to look for identifier field in the dict - return string_relation_func(identifier_value['identifier']) - raise ValidationError({ relation_field: [ - 'Relation dict does not have any fields to identify relation with (id or identifier)'] }) + return string_relation_func(identifier_value["identifier"]) + raise ValidationError( + { + relation_field: [ + "Relation dict does not have any fields to identify relation with (id or identifier)" + ] + } + ) else: - _logger.error('is_valid() field validation for relation %s: unexpected type: %s' - % (relation_field, type(identifier_value))) - raise ValidationError('Validation error for relation %s. Data in unexpected format' % relation_field) + _logger.error( + "is_valid() field validation for relation %s: unexpected type: %s" + % (relation_field, type(identifier_value)) + ) + raise ValidationError( + "Validation error for relation %s. Data in unexpected format" % relation_field + ) def _request_by_end_user(self): - return 'request' in self.context and not self.context['request'].user.is_service + return "request" in self.context and not self.context["request"].user.is_service def _request_by_service(self): - return 'request' in self.context and self.context['request'].user.is_service + return "request" in self.context and self.context["request"].user.is_service -class LightSerializer(): +class LightSerializer: """ LightSerializer is optimized for speed for read-only serializing of a @@ -302,15 +320,23 @@ def ls_field_list(cls, received_field_list=[]): This default method only filters out fields that are not ine the allowed_fields list. """ - assert isinstance(cls.allowed_fields, set), 'light serializer must specify allowed_fields as a set()' + assert isinstance( + cls.allowed_fields, set + ), "light serializer must specify allowed_fields as a set()" if received_field_list: # ensure only allowed fields and no rubbish ends up in the query - unknown # fields to the db will cause crash. - field_list = [ field for field in received_field_list if field in cls.allowed_fields ] + field_list = [field for field in received_field_list if field in cls.allowed_fields] if not field_list: - raise Http400({ 'detail': ['uh oh, none of the fields you requested are listed in allowed_fields. ' - 'received fields: %s' % str(received_field_list)] }) + raise Http400( + { + "detail": [ + "uh oh, none of the fields you requested are listed in allowed_fields. " + "received fields: %s" % str(received_field_list) + ] + } + ) else: # get all fields @@ -328,9 +354,17 @@ def serialize(cls, unserialized_data): The end result is supposed to look the same as normally from a serializer. """ - assert type(unserialized_data) in (QuerySet, dict, list), 'unserialized_data type must be QuerySet or dict' - assert isinstance(cls.special_fields, set), 'light serializer must specify special_fields as a set()' - assert isinstance(cls.relation_fields, set), 'light serializer must specify relation_fields as a set()' + assert type(unserialized_data) in ( + QuerySet, + dict, + list, + ), "unserialized_data type must be QuerySet or dict" + assert isinstance( + cls.special_fields, set + ), "light serializer must specify special_fields as a set()" + assert isinstance( + cls.relation_fields, set + ), "light serializer must specify relation_fields as a set()" special_fields = cls.special_fields relation_fields = cls.relation_fields @@ -338,7 +372,7 @@ def serialize(cls, unserialized_data): _relation_id_fields = cls._relation_id_fields for field in relation_fields: - _relation_id_fields.add('%s_id' % field) + _relation_id_fields.add("%s_id" % field) if isinstance(unserialized_data, (QuerySet, list)): unserialized_data = unserialized_data @@ -359,18 +393,18 @@ def serialize(cls, unserialized_data): continue elif field in special_fields: serialize_special_field(item, field, value) - elif '__' in field: + elif "__" in field: # fields from relations, such as parent_directory__identifier - field_sections = field.split('__') + field_sections = field.split("__") field_name = field_sections[0] - if field.endswith('_json'): + if field.endswith("_json"): # _json -ending fields are a special case. only handles # fields identifier and id. if field_name not in item: item[field_name] = {} if isinstance(value, dict): # take identifier from inside the json-field - item[field_name] = { 'identifier': value['identifier'] } + item[field_name] = {"identifier": value["identifier"]} else: # id item[field_name][field_sections[-1]] = value @@ -379,13 +413,12 @@ def serialize(cls, unserialized_data): try: item[field_name][field_sections[-1]] = value except KeyError: - item[field_name] = { field_sections[-1]: value } + item[field_name] = {field_sections[-1]: value} elif field in _relation_id_fields: raise ValueError( - 'LightSerializer received field: %s. Expected: %s__attrname. ' - 'Did you forget to pass result of ls_field_list() to queryset ' - '.values(*field_list)?' - % (field, field[:-3]) + "LightSerializer received field: %s. Expected: %s__attrname. " + "Did you forget to pass result of ls_field_list() to queryset " + ".values(*field_list)?" % (field, field[:-3]) ) else: if isinstance(value, datetime): diff --git a/src/metax_api/api/rest/base/serializers/contract_serializer.py b/src/metax_api/api/rest/base/serializers/contract_serializer.py index ad8ad87f..9fa4e167 100755 --- a/src/metax_api/api/rest/base/serializers/contract_serializer.py +++ b/src/metax_api/api/rest/base/serializers/contract_serializer.py @@ -14,22 +14,21 @@ class ContractSerializer(CommonSerializer): - class Meta: model = Contract fields = ( - 'id', - 'contract_json', + "id", + "contract_json", ) + CommonSerializer.Meta.fields extra_kwargs = CommonSerializer.Meta.extra_kwargs def validate_contract_json(self, value): - validate_json(value, self.context['view'].json_schema) + validate_json(value, self.context["view"].json_schema) if self._operation_is_create: self._validate_identifier_uniqueness(value) return value def _validate_identifier_uniqueness(self, contract_json): - if Contract.objects.filter(contract_json__identifier=contract_json['identifier']).exists(): + if Contract.objects.filter(contract_json__identifier=contract_json["identifier"]).exists(): raise ValidationError(f"identifier {contract_json['identifier']} already exists") diff --git a/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py b/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py index 8491c012..bd6294ad 100755 --- a/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py +++ b/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py @@ -17,50 +17,56 @@ class DataCatalogSerializer(CommonSerializer): - class Meta: model = DataCatalog fields = ( - 'id', - 'catalog_json', - 'catalog_record_group_edit', - 'catalog_record_group_create', - 'catalog_record_services_edit', - 'catalog_record_services_create', - 'catalog_record_group_read', - 'catalog_record_services_read', + "id", + "catalog_json", + "catalog_record_group_edit", + "catalog_record_group_create", + "catalog_record_services_edit", + "catalog_record_services_create", + "catalog_record_group_read", + "catalog_record_services_read", ) + CommonSerializer.Meta.fields extra_kwargs = CommonSerializer.Meta.extra_kwargs def is_valid(self, raise_exception=False): super(DataCatalogSerializer, self).is_valid(raise_exception=raise_exception) - if 'catalog_json' in self.initial_data: + if "catalog_json" in self.initial_data: self._validate_dataset_schema() - if self.initial_data['catalog_json'].get('dataset_versioning', False) is True \ - and self.initial_data['catalog_json'].get('harvested', False) is True: - raise ValidationError({ - 'detail': ['versioning cannot be enabled in harvested catalogs.'] - }) + if ( + self.initial_data["catalog_json"].get("dataset_versioning", False) is True + and self.initial_data["catalog_json"].get("harvested", False) is True + ): + raise ValidationError( + {"detail": ["versioning cannot be enabled in harvested catalogs."]} + ) def validate_catalog_json(self, value): - DCS.validate_reference_data(value, self.context['view'].cache) + DCS.validate_reference_data(value, self.context["view"].cache) # Validate against json schema only after reference data has been populated - validate_json(value, self.context['view'].json_schema) + validate_json(value, self.context["view"].json_schema) if self._operation_is_create: self._validate_identifier_uniqueness(value) return value def _validate_dataset_schema(self): - rd_schema = self.initial_data['catalog_json'].get('research_dataset_schema', None) + rd_schema = self.initial_data["catalog_json"].get("research_dataset_schema", None) if not rd_schema: return - schema_path = '%s/../schemas/%s_dataset_schema.json' % (path.dirname(__file__), rd_schema) + schema_path = "%s/../schemas/%s_dataset_schema.json" % ( + path.dirname(__file__), + rd_schema, + ) if not path.isfile(schema_path): - raise ValidationError({'catalog_json': ['research dataset schema \'%s\' not found' % rd_schema]}) + raise ValidationError( + {"catalog_json": ["research dataset schema '%s' not found" % rd_schema]} + ) def _validate_identifier_uniqueness(self, catalog_json): - if DataCatalog.objects.filter(catalog_json__identifier=catalog_json['identifier']).exists(): - raise ValidationError({'identifier': - ['identifier %s already exists' % catalog_json['identifier']] - }) \ No newline at end of file + if DataCatalog.objects.filter(catalog_json__identifier=catalog_json["identifier"]).exists(): + raise ValidationError( + {"identifier": ["identifier %s already exists" % catalog_json["identifier"]]} + ) diff --git a/src/metax_api/api/rest/base/serializers/directory_serializer.py b/src/metax_api/api/rest/base/serializers/directory_serializer.py index 14dabfd2..7f467f25 100755 --- a/src/metax_api/api/rest/base/serializers/directory_serializer.py +++ b/src/metax_api/api/rest/base/serializers/directory_serializer.py @@ -29,28 +29,29 @@ class DirectorySerializer(CommonSerializer): class Meta: model = Directory fields = ( - 'id', - 'byte_size', - 'directory_deleted', - 'directory_modified', - 'directory_name', - 'directory_path', - 'file_count', - 'identifier', - 'parent_directory', - 'project_identifier', - 'user_modified', - 'date_modified', - 'user_created', - 'date_created', + "id", + "byte_size", + "directory_deleted", + "directory_modified", + "directory_name", + "directory_path", + "file_count", + "identifier", + "parent_directory", + "project_identifier", + "user_modified", + "date_modified", + "user_created", + "date_created", ) + CommonSerializer.Meta.fields extra_kwargs = CommonSerializer.Meta.extra_kwargs def is_valid(self, raise_exception=False): - if 'parent_directory' in self.initial_data: - self.initial_data['parent_directory'] = self._get_id_from_related_object( - 'parent_directory', self._get_parent_directory_relation) + if "parent_directory" in self.initial_data: + self.initial_data["parent_directory"] = self._get_id_from_related_object( + "parent_directory", self._get_parent_directory_relation + ) super(DirectorySerializer, self).is_valid(raise_exception=raise_exception) def validate_directory_path(self, value): @@ -62,25 +63,30 @@ def validate_directory_path(self, value): since directories can be frozen and unfrozen multiple times. """ dir_exists_in_project_scope = Directory.objects.filter( - project_identifier=self.initial_data['project_identifier'], - directory_path=self.initial_data['directory_path'] + project_identifier=self.initial_data["project_identifier"], + directory_path=self.initial_data["directory_path"], ).exists() if dir_exists_in_project_scope: - raise ValidationError([ - 'directory path %s already exists in project %s scope. Are you trying to freeze same directory again?' - % (self.initial_data['directory_path'], self.initial_data['project_identifier']) - ]) + raise ValidationError( + [ + "directory path %s already exists in project %s scope. Are you trying to freeze same directory again?" + % ( + self.initial_data["directory_path"], + self.initial_data["project_identifier"], + ) + ] + ) return value def to_representation(self, instance): res = super(DirectorySerializer, self).to_representation(instance) - if 'parent_directory' in res and instance.parent_directory: - res['parent_directory'] = { - 'id': instance.parent_directory.id, - 'identifier': instance.parent_directory.identifier, + if "parent_directory" in res and instance.parent_directory: + res["parent_directory"] = { + "id": instance.parent_directory.id, + "identifier": instance.parent_directory.identifier, } return res @@ -90,18 +96,20 @@ def _get_parent_directory_relation(self, identifier_value): Passed to _get_id_from_related_object() to be used when relation was a string identifier """ if isinstance(identifier_value, dict): - identifier_value = identifier_value['identifier'] + identifier_value = identifier_value["identifier"] try: return Directory.objects.get(identifier=identifier_value).id except Directory.DoesNotExist: - raise ValidationError({ 'parent_directory': ['identifier %s not found' % str(identifier_value)]}) + raise ValidationError( + {"parent_directory": ["identifier %s not found" % str(identifier_value)]} + ) class LightDirectorySerializer(LightSerializer): allowed_fields = set(DirectorySerializer.Meta.fields) special_fields = set() - relation_fields = set(['parent_directory']) + relation_fields = set(["parent_directory"]) @classmethod def ls_field_list(cls, received_field_list=[]): @@ -116,8 +124,8 @@ def ls_field_list(cls, received_field_list=[]): continue elif field in cls.relation_fields: # parent directory - return_file_fields.append('%s__identifier' % field) - return_file_fields.append('%s__id' % field) + return_file_fields.append("%s__identifier" % field) + return_file_fields.append("%s__id" % field) else: return_file_fields.append(field) diff --git a/src/metax_api/api/rest/base/serializers/file_serializer.py b/src/metax_api/api/rest/base/serializers/file_serializer.py index 40706cf6..04e66973 100755 --- a/src/metax_api/api/rest/base/serializers/file_serializer.py +++ b/src/metax_api/api/rest/base/serializers/file_serializer.py @@ -24,22 +24,22 @@ END_USER_UPDATE_ALLOWED_FIELDS = [ - 'file_characteristics', - + "file_characteristics", # not set by the user, but are set by metax, so should not be discarded - 'date_modified', - 'user_modified', - 'service_modified', - '__request' + "date_modified", + "user_modified", + "service_modified", + "__request", ] CHECKSUM_ALGORITHMS = settings.CHECKSUM_ALGORITHMS + class FileSerializer(CommonSerializer): - checksum_fields = set(['algorithm', 'checked', 'value']) + checksum_fields = set(["algorithm", "checked", "value"]) # has to be present to not break rest_framework browsabale api - checksum = serializers.CharField(source='checksum_value', read_only=True) + checksum = serializers.CharField(source="checksum_value", read_only=True) # file paths must preserve any leading or trailing whitespace, as those # are valid characters in a filename. note: file_path is actually a @@ -50,34 +50,38 @@ class FileSerializer(CommonSerializer): # identifier is not set as unique in the db due to having to allow removed files exist. # use the same validator that would otherwise automatically be used, to verify uniqueness # among non-removed files. - identifier = serializers.CharField(validators=[UniqueValidator( - queryset=File.objects.all(), - message='a file with given identifier already exists' - )]) + identifier = serializers.CharField( + validators=[ + UniqueValidator( + queryset=File.objects.all(), + message="a file with given identifier already exists", + ) + ] + ) class Meta: model = File fields = ( - 'id', - 'byte_size', - 'checksum', - 'checksum_algorithm', - 'checksum_checked', - 'checksum_value', - 'parent_directory', - 'file_deleted', - 'file_frozen', - 'file_format', - 'file_modified', - 'file_name', - 'file_path', - 'file_storage', - 'file_uploaded', - 'identifier', - 'file_characteristics', - 'file_characteristics_extension', - 'open_access', - 'project_identifier', + "id", + "byte_size", + "checksum", + "checksum_algorithm", + "checksum_checked", + "checksum_value", + "parent_directory", + "file_deleted", + "file_frozen", + "file_format", + "file_modified", + "file_name", + "file_path", + "file_storage", + "file_uploaded", + "identifier", + "file_characteristics", + "file_characteristics_extension", + "open_access", + "project_identifier", ) + CommonSerializer.Meta.fields extra_kwargs = CommonSerializer.Meta.extra_kwargs @@ -86,53 +90,57 @@ def is_valid(self, raise_exception=False): if self._request_by_end_user(): self._end_user_update_validations(self.initial_data) - if 'file_storage' in self.initial_data: - self.initial_data['file_storage'] = self._get_id_from_related_object( - 'file_storage', self._get_file_storage_relation) + if "file_storage" in self.initial_data: + self.initial_data["file_storage"] = self._get_id_from_related_object( + "file_storage", self._get_file_storage_relation + ) - if 'checksum' in self.initial_data: - self._flatten_checksum(self.initial_data['checksum']) + if "checksum" in self.initial_data: + self._flatten_checksum(self.initial_data["checksum"]) - if 'parent_directory' in self.initial_data: - self.initial_data['parent_directory'] = self._get_id_from_related_object( - 'parent_directory', self._get_parent_directory_relation) + if "parent_directory" in self.initial_data: + self.initial_data["parent_directory"] = self._get_id_from_related_object( + "parent_directory", self._get_parent_directory_relation + ) - if ('file_characteristics' in self.initial_data and 'format_version' - in self.initial_data['file_characteristics']): - if self.initial_data['file_characteristics']['format_version'] == "": - self.initial_data['file_characteristics'].pop('format_version', None) + if ( + "file_characteristics" in self.initial_data + and "format_version" in self.initial_data["file_characteristics"] + ): + if self.initial_data["file_characteristics"]["format_version"] == "": + self.initial_data["file_characteristics"].pop("format_version", None) super(FileSerializer, self).is_valid(raise_exception=raise_exception) def to_representation(self, instance): res = super(FileSerializer, self).to_representation(instance) - if 'file_storage' in res: - if self.expand_relation_requested('file_storage'): - res['file_storage'] = FileStorageSerializer(instance.file_storage).data + if "file_storage" in res: + if self.expand_relation_requested("file_storage"): + res["file_storage"] = FileStorageSerializer(instance.file_storage).data else: - res['file_storage'] = { - 'id': instance.file_storage.id, - 'identifier': instance.file_storage.file_storage_json['identifier'], + res["file_storage"] = { + "id": instance.file_storage.id, + "identifier": instance.file_storage.file_storage_json["identifier"], } - if 'parent_directory' in res: - if self.expand_relation_requested('parent_directory'): - res['parent_directory'] = DirectorySerializer(instance.parent_directory).data + if "parent_directory" in res: + if self.expand_relation_requested("parent_directory"): + res["parent_directory"] = DirectorySerializer(instance.parent_directory).data else: - res['parent_directory'] = { - 'id': instance.parent_directory.id, - 'identifier': instance.parent_directory.identifier, + res["parent_directory"] = { + "id": instance.parent_directory.id, + "identifier": instance.parent_directory.identifier, } - if not self.requested_fields or any('checksum' in f for f in self.requested_fields): - res['checksum'] = self.form_checksum(res) + if not self.requested_fields or any("checksum" in f for f in self.requested_fields): + res["checksum"] = self.form_checksum(res) return res def validate_file_characteristics(self, value): - validate_json(value, self.context['view'].json_schema) - FS.validate_file_characteristics_reference_data(value, self.context['view'].cache) + validate_json(value, self.context["view"].json_schema) + FS.validate_file_characteristics_reference_data(value, self.context["view"].cache) return value def validate_file_path(self, value): @@ -140,26 +148,28 @@ def validate_file_path(self, value): Ensure file_path is unique in the project, within unremoved files. file_path can exist multiple times for removed files though. """ - if hasattr(self, 'file_path_checked'): + if hasattr(self, "file_path_checked"): # has been previously validated during bulk operation processing. # saves a fetch to the db. return value if self._operation_is_create: - if 'project_identifier' not in self.initial_data: + if "project_identifier" not in self.initial_data: # the validation for project_identifier is executed later... return value - project = self.initial_data['project_identifier'] + project = self.initial_data["project_identifier"] if File.objects.filter(project_identifier=project, file_path=value).exists(): - raise ValidationError('a file with path %s already exists in project %s' % (value, project)) + raise ValidationError( + "a file with path %s already exists in project %s" % (value, project) + ) elif self._operation_is_update: - if 'file_path' not in self.initial_data: + if "file_path" not in self.initial_data: return value - if self.instance.file_path != self.initial_data['file_path']: + if self.instance.file_path != self.initial_data["file_path"]: # would require re-arranging the virtual file tree... implement in the future if need arises - raise ValidationError('file_path can not be changed after creating') + raise ValidationError("file_path can not be changed after creating") else: # delete pass @@ -172,8 +182,11 @@ def validate_checksum_algorithm(self, value): """ if value not in CHECKSUM_ALGORITHMS: - raise ValidationError('file checksum_algorithm should be one of {}, now {}' - .format(CHECKSUM_ALGORITHMS, value)) + raise ValidationError( + "file checksum_algorithm should be one of {}, now {}".format( + CHECKSUM_ALGORITHMS, value + ) + ) return value @@ -181,7 +194,9 @@ def _end_user_update_validations(self, validated_data): """ Enforce some rules related to end users when updating files. """ - fields_to_discard = [ key for key in validated_data.keys() if key not in END_USER_UPDATE_ALLOWED_FIELDS ] + fields_to_discard = [ + key for key in validated_data.keys() if key not in END_USER_UPDATE_ALLOWED_FIELDS + ] for field_name in fields_to_discard: del validated_data[field_name] @@ -190,13 +205,15 @@ def _get_file_storage_relation(self, identifier_value): Passed to _get_id_from_related_object() to be used when relation was a string identifier """ if isinstance(identifier_value, dict): - identifier_value = identifier_value['file_storage_json']['identifier'] + identifier_value = identifier_value["file_storage_json"]["identifier"] try: return FileStorage.objects.get( - file_storage_json__contains={ 'identifier': identifier_value } + file_storage_json__contains={"identifier": identifier_value} ).id except FileStorage.DoesNotExist: - raise ValidationError({ 'file_storage': ['identifier %s not found' % str(identifier_value)]}) + raise ValidationError( + {"file_storage": ["identifier %s not found" % str(identifier_value)]} + ) def _flatten_checksum(self, checksum): """ @@ -215,7 +232,7 @@ def _flatten_checksum(self, checksum): """ for key in self.checksum_fields: if key in checksum: - self.initial_data['checksum_%s' % key] = checksum[key] + self.initial_data["checksum_%s" % key] = checksum[key] @classmethod def form_checksum(cls, file_data): @@ -225,7 +242,7 @@ def form_checksum(cls, file_data): """ checksum = {} for key in cls.checksum_fields: - checksum_field = 'checksum_%s' % key + checksum_field = "checksum_%s" % key if checksum_field in file_data: checksum[key] = file_data[checksum_field] file_data.pop(checksum_field) @@ -236,18 +253,20 @@ def _get_parent_directory_relation(self, identifier_value): Passed to _get_id_from_related_object() to be used when relation was a string identifier """ if isinstance(identifier_value, dict): - identifier_value = identifier_value['identifier'] + identifier_value = identifier_value["identifier"] try: return Directory.objects.get(identifier=identifier_value).id except Directory.DoesNotExist: - raise ValidationError({ 'parent_directory': ['identifier %s not found' % str(identifier_value)]}) + raise ValidationError( + {"parent_directory": ["identifier %s not found" % str(identifier_value)]} + ) class LightFileSerializer(LightSerializer): allowed_fields = set(FileSerializer.Meta.fields) - relation_fields = set(['file_storage', 'parent_directory']) - special_fields = set([ 'checksum_%s' % field for field in FileSerializer.checksum_fields ]) + relation_fields = set(["file_storage", "parent_directory"]) + special_fields = set(["checksum_%s" % field for field in FileSerializer.checksum_fields]) @classmethod def ls_field_list(cls, received_field_list=[]): @@ -257,24 +276,24 @@ def ls_field_list(cls, received_field_list=[]): field_list = super().ls_field_list(received_field_list) if not received_field_list: # a field of this name does not exist in the db - remove or will cause crash - field_list.discard('checksum') + field_list.discard("checksum") return_file_fields = [] for field in field_list: if field not in cls.allowed_fields: continue - elif field == 'checksum': + elif field == "checksum": for checksum_field in FileSerializer.checksum_fields: - return_file_fields.append('checksum_%s' % checksum_field) + return_file_fields.append("checksum_%s" % checksum_field) break elif field in cls.relation_fields: - if field == 'file_storage': - return_file_fields.append('%s__file_storage_json' % field) - return_file_fields.append('%s__id' % field) + if field == "file_storage": + return_file_fields.append("%s__file_storage_json" % field) + return_file_fields.append("%s__id" % field) else: # parent directory - return_file_fields.append('%s__identifier' % field) - return_file_fields.append('%s__id' % field) + return_file_fields.append("%s__identifier" % field) + return_file_fields.append("%s__id" % field) else: return_file_fields.append(field) @@ -282,8 +301,8 @@ def ls_field_list(cls, received_field_list=[]): @staticmethod def serialize_special_field(file, field, value): - if field.startswith('checksum'): + if field.startswith("checksum"): try: - file['checksum'][field.split('_')[1]] = value + file["checksum"][field.split("_")[1]] = value except KeyError: - file['checksum'] = { field.split('_')[1]: value } + file["checksum"] = {field.split("_")[1]: value} diff --git a/src/metax_api/api/rest/base/serializers/file_storage_serializer.py b/src/metax_api/api/rest/base/serializers/file_storage_serializer.py index f8286175..093993a3 100755 --- a/src/metax_api/api/rest/base/serializers/file_storage_serializer.py +++ b/src/metax_api/api/rest/base/serializers/file_storage_serializer.py @@ -14,25 +14,26 @@ class FileStorageSerializer(CommonSerializer): - class Meta: model = FileStorage fields = ( - 'id', - 'file_storage_json', + "id", + "file_storage_json", ) + CommonSerializer.Meta.fields extra_kwargs = CommonSerializer.Meta.extra_kwargs def validate_file_storage_json(self, value): - validate_json(value, self.context['view'].json_schema) + validate_json(value, self.context["view"].json_schema) if self._operation_is_create: self._validate_identifier_uniqueness(value) return value def _validate_identifier_uniqueness(self, file_storage_json): - if FileStorage.objects.filter(file_storage_json__identifier=file_storage_json['identifier']).exists(): - raise ValidationError({'identifier': - ['identifier %s already exists' % file_storage_json['identifier']] - }) + if FileStorage.objects.filter( + file_storage_json__identifier=file_storage_json["identifier"] + ).exists(): + raise ValidationError( + {"identifier": ["identifier %s already exists" % file_storage_json["identifier"]]} + ) diff --git a/src/metax_api/api/rest/base/serializers/serializer_utils.py b/src/metax_api/api/rest/base/serializers/serializer_utils.py index 88a9355a..1f457874 100755 --- a/src/metax_api/api/rest/base/serializers/serializer_utils.py +++ b/src/metax_api/api/rest/base/serializers/serializer_utils.py @@ -12,16 +12,13 @@ from jsonschema.exceptions import ValidationError as JsonValidationError from rest_framework.serializers import ValidationError -date_re = re.compile( - r'^\d{4}-\d{2}-\d{2}$' -) +date_re = re.compile(r"^\d{4}-\d{2}-\d{2}$") datetime_re = re.compile( - r'^\d{4}-\d{2}-\d{2}T' - r'\d{2}:\d{2}:\d{2}(\.\d{1,9})?' - r'(Z|[+-]\d{2}:\d{2})$' + r"^\d{4}-\d{2}-\d{2}T" r"\d{2}:\d{2}:\d{2}(\.\d{1,9})?" r"(Z|[+-]\d{2}:\d{2})$" ) + def validate_json(value, schema): """ Since RFC3339 dependency was removed, date and datetime formats have to be validated @@ -41,10 +38,13 @@ def validate_json(value, schema): path = e.path schema = e.schema - raise ValidationError('%s. Json path: %s. Schema: %s' % (message, [p for p in path], schema)) + raise ValidationError( + "%s. Json path: %s. Schema: %s" % (message, [p for p in path], schema) + ) + # helper methods for date and datetime validation -@FormatChecker.checks(FormatChecker, format='date') +@FormatChecker.checks(FormatChecker, format="date") def date(value): if isinstance(value, str_types): match = date_re.match(value) @@ -53,7 +53,8 @@ def date(value): return False -@FormatChecker.checks(FormatChecker, format='date-time') + +@FormatChecker.checks(FormatChecker, format="date-time") def date_time(value): if isinstance(value, str_types): match = datetime_re.match(value) diff --git a/src/metax_api/api/rest/base/serializers/xml_metadata_serializer.py b/src/metax_api/api/rest/base/serializers/xml_metadata_serializer.py index c6aa042b..e7f5845c 100755 --- a/src/metax_api/api/rest/base/serializers/xml_metadata_serializer.py +++ b/src/metax_api/api/rest/base/serializers/xml_metadata_serializer.py @@ -11,23 +11,20 @@ class XmlMetadataSerializer(CommonSerializer): - class Meta: model = XmlMetadata fields = ( - 'id', - 'namespace', - 'xml', - 'file', + "id", + "namespace", + "xml", + "file", ) + CommonSerializer.Meta.fields extra_kwargs = CommonSerializer.Meta.extra_kwargs def to_representation(self, instance): res = super(XmlMetadataSerializer, self).to_representation(instance) - res['file'] = { - 'identifier': self.instance.file.identifier - } + res["file"] = {"identifier": self.instance.file.identifier} return res def validate_xml(self, value): diff --git a/src/metax_api/api/rest/base/views/__init__.py b/src/metax_api/api/rest/base/views/__init__.py index 760b837e..d5c62dd9 100755 --- a/src/metax_api/api/rest/base/views/__init__.py +++ b/src/metax_api/api/rest/base/views/__init__.py @@ -5,7 +5,6 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from .api_error_view import ApiErrorViewSet from .common_view import CommonViewSet from .contract_view import ContractViewSet from .data_catalog_view import DataCatalogViewSet diff --git a/src/metax_api/api/rest/base/views/api_error_view.py b/src/metax_api/api/rest/base/views/api_error_view.py deleted file mode 100755 index b9612ec3..00000000 --- a/src/metax_api/api/rest/base/views/api_error_view.py +++ /dev/null @@ -1,84 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -import logging - -from django.http import Http404 -from rest_framework.decorators import action -from rest_framework.response import Response - -from metax_api.exceptions import Http403, Http501 -from metax_api.services import ApiErrorService - -from ..serializers import FileSerializer -from .common_view import CommonViewSet - -""" -An API to browse error files and retrieve complete error details. - -Allows only reading and deleting by user 'metax', inaccessible to everyone else. - -Not connected to the DB in any way, deals directly with files saved in the designated -error file location. -""" - - -_logger = logging.getLogger(__name__) - - -class ApiErrorViewSet(CommonViewSet): - - filter_backends = () - - # this serves no purpose, but strangely in local dev browsable api for /rest/apierrors/pid - # works fine, while in metax-test it will throw an error complaining about a missing serializer. - serializer_class = FileSerializer - - def initial(self, request, *args, **kwargs): - if request.user.username != 'metax': - raise Http403 - return super(ApiErrorViewSet, self).initial(request, *args, **kwargs) - - def get_queryset(self): - return self.list(None) - - def retrieve(self, request, *args, **kwargs): - try: - error_details = ApiErrorService.retrieve_error_details(kwargs['pk']) - except: - raise Http404 - return Response(data=error_details, status=200) - - def list(self, request, *args, **kwargs): - error_list = ApiErrorService.retrieve_error_list() - return Response(data=error_list, status=200) - - def destroy(self, request, *args, **kwargs): - _logger.info('DELETE %s called by %s' % (request.META['PATH_INFO'], request.user.username)) - ApiErrorService.remove_error_file(kwargs['pk']) - return Response(status=204) - - @action(detail=False, methods=['post'], url_path="flush") - def flush_errors(self, request): - _logger.info('%s called by %s' % (request.META['PATH_INFO'], request.user.username)) - files_deleted_count = ApiErrorService.flush_errors() - return Response(data={ 'files_deleted': files_deleted_count }, status=200) - - def update(self, request, *args, **kwargs): - raise Http501() - - def update_bulk(self, request, *args, **kwargs): - raise Http501() - - def partial_update(self, request, *args, **kwargs): - raise Http501() - - def partial_update_bulk(self, request, *args, **kwargs): - raise Http501() - - def create(self, request, *args, **kwargs): - raise Http501() diff --git a/src/metax_api/api/rest/base/views/common_view.py b/src/metax_api/api/rest/base/views/common_view.py index 79bb4b84..14de18ca 100755 --- a/src/metax_api/api/rest/base/views/common_view.py +++ b/src/metax_api/api/rest/base/views/common_view.py @@ -16,14 +16,20 @@ from rest_framework.views import set_rollback from rest_framework.viewsets import ModelViewSet +from metax_api.api.rest.v2.serializers import ApiErrorSerializerV2 from metax_api.exceptions import Http400, Http403, Http500 from metax_api.permissions import EndUserPermissions, ServicePermissions -from metax_api.services import ApiErrorService, CallableService, CommonService as CS, RedisCacheService +from metax_api.services import ( + CallableService, + CommonService as CS, + RabbitMQService as rabbitmq, + RedisCacheService, +) _logger = logging.getLogger(__name__) RESPONSE_SUCCESS_CODES = (200, 201, 204) -WRITE_OPERATIONS = ('PUT', 'PATCH', 'POST') +WRITE_OPERATIONS = ("PUT", "PATCH", "POST") class CommonViewSet(ModelViewSet): @@ -33,7 +39,7 @@ class CommonViewSet(ModelViewSet): which include fields like modified and created timestamps, uuid, active flags etc. """ - api_type = 'rest' + api_type = "rest" authentication_classes = () permission_classes = [EndUserPermissions, ServicePermissions] @@ -53,7 +59,9 @@ class CommonViewSet(ModelViewSet): def __init__(self, *args, **kwargs): super(CommonViewSet, self).__init__(*args, **kwargs) - if (hasattr(self, 'object') and self.object) and (not hasattr(self, 'queryset') or self.queryset is None): + if (hasattr(self, "object") and self.object) and ( + not hasattr(self, "queryset") or self.queryset is None + ): # ^ must have attribute 'object' set, AND queryset not set. # the primary location where a queryset is initialized for @@ -67,7 +75,7 @@ def dispatch(self, request, **kwargs): res = super().dispatch(request, **kwargs) if res.status_code in RESPONSE_SUCCESS_CODES: - if CS.get_boolean_query_param(self.request, 'dryrun'): + if CS.get_boolean_query_param(self.request, "dryrun"): # with dryrun parameter: # - nothing must be saved into db # - no events must escape from metax to other services, such as rabbitmq @@ -87,7 +95,8 @@ def get_permissions(self): Instantiates and returns the list of permissions that this view requires. """ return [ - permission() for permission in self.permission_classes + permission() + for permission in self.permission_classes if permission.service_permission == self.request.user.is_service ] @@ -111,8 +120,8 @@ def handle_exception(self, exc): # must convert any standard python exceptions to framework-recognized # exceptions, so that the following handle_exception() goes the expected # path, and prepares for db dollback, sets some headers etc. - _logger.exception('Internal Server Error') - exc = Http500({ 'detail': ['Internal Server Error'] }) + _logger.exception("Internal Server Error") + exc = Http500({"detail": ["Internal Server Error"]}) try: # fyi: when an error occurs during a request, and ATOMIC_REQUESTS=True, @@ -121,24 +130,32 @@ def handle_exception(self, exc): response = super(CommonViewSet, self).handle_exception(exc) except: # for when absolutely everything has gone wrong... - _logger.exception('Exception while trying to handle original exception: %s' % str(exc)) + _logger.exception("Exception while trying to handle original exception: %s" % str(exc)) set_rollback() - response = Response({ 'detail': ['Internal Server Error'] }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + response = Response( + {"detail": ["Internal Server Error"]}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) if type(exc) not in (Http403, Http404, PermissionDenied, MethodNotAllowed): - ApiErrorService.store_error_details(self.request, response, exc) + try: + error_json = ApiErrorSerializerV2.request_to_json(self.request, response) + response.data["error_identifier"] = error_json["identifier"] + rabbitmq.publish(error_json, exchange="apierrors") + except Exception as e: + _logger.error(f"could not send api error to rabbitmq. Error: {e}") return response # TODO: supporting both parameters over a transition period and eventually will get rid of no_pagination. def paginate_queryset(self, queryset): keys = self.request.query_params.keys() - if 'pagination' in keys: - if not CS.get_boolean_query_param(self.request, 'pagination'): + if "pagination" in keys: + if not CS.get_boolean_query_param(self.request, "pagination"): return None return super(CommonViewSet, self).paginate_queryset(queryset) - elif 'no_pagination' in keys: - if CS.get_boolean_query_param(self.request, 'no_pagination'): + elif "no_pagination" in keys: + if CS.get_boolean_query_param(self.request, "no_pagination"): return None return super(CommonViewSet, self).paginate_queryset(queryset) else: @@ -153,24 +170,29 @@ def get_queryset(self): """ additional_filters = {} q_filters = [] + deduplicated_q_filters = [] CS.set_if_modified_since_filter(self.request, additional_filters) - if hasattr(self, 'queryset_search_params'): + if hasattr(self, "queryset_search_params"): additional_filters.update(**self.queryset_search_params) - if 'q_filters' in additional_filters: + if "q_filters" in additional_filters: # Q-filter objects, which can contain more complex filter options such as OR-clauses - q_filters = additional_filters.pop('q_filters') + q_filters = additional_filters.pop("q_filters") - if CS.get_boolean_query_param(self.request, 'removed'): - additional_filters.update({'removed': True}) + if "deduplicated_q_filters" in additional_filters: + # Q-filter objects that may produce duplicate results + deduplicated_q_filters = additional_filters.pop("deduplicated_q_filters") + + if CS.get_boolean_query_param(self.request, "removed"): + additional_filters.update({"removed": True}) self.queryset = self.queryset_unfiltered - if 'fields' in self.request.query_params: + if "fields" in self.request.query_params: if not self.fields: # save fields when no inheriting view has done it yet - self.fields = self.request.query_params['fields'].split(',') + self.fields = self.request.query_params["fields"].split(",") for field in self.fields: if field not in self.get_serializer_class().Meta.fields: @@ -181,18 +203,24 @@ def get_queryset(self): # check if requested fields are relations, so that we know to include them in select_related. # if no fields is relation, select_related will be made empty. - self.select_related = [ rel for rel in self.select_related if rel in self.fields ] + self.select_related = [rel for rel in self.select_related if rel in self.fields] - queryset = super().get_queryset().filter(*q_filters, **additional_filters) + queryset = super().get_queryset() + if deduplicated_q_filters: + # run filters that may produce duplicates and deduplicate the results. deduplicating just the ids + # in a subquery is faster than deduplicating the full results when there are a lot of duplicates. + id_query = queryset.filter(*deduplicated_q_filters).values("id").distinct() + queryset = queryset.filter(id__in=id_query) + queryset = queryset.filter(*q_filters, **additional_filters) - if self.request.META['REQUEST_METHOD'] in WRITE_OPERATIONS: + if self.request.META["REQUEST_METHOD"] in WRITE_OPERATIONS: # for update operations, do not select relations in the original queryset # so that select_for_update() can be used to lock the row for the duration # of the update-operation. when the full object is returned, it is possible # that additional queres need to be executed to the db to retrieve relation # data, but that seems to be the price to pay to be able the lock rows being # written to. - queryset = queryset.select_for_update(nowait=False, of=('self',)) + queryset = queryset.select_for_update(nowait=False, of=("self",)) else: queryset = queryset.select_related(*self.select_related) @@ -205,12 +233,14 @@ def get_object(self, search_params=None): param search_params: pass a custom filter instead of using the default search mechanism """ - if CS.get_boolean_query_param(self.request, 'removed'): + if CS.get_boolean_query_param(self.request, "removed"): return self.get_removed_object(search_params=search_params) elif search_params: filter_kwargs = search_params else: - if CS.is_primary_key(self.kwargs.get(self.lookup_field, False)) or not hasattr(self, 'lookup_field_other'): + if CS.is_primary_key(self.kwargs.get(self.lookup_field, False)) or not hasattr( + self, "lookup_field_other" + ): # lookup by originak lookup_field. standard django procedure lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field else: @@ -220,7 +250,7 @@ def get_object(self, search_params=None): # replace original field name with field name in lookup_field_other self.kwargs[lookup_url_kwarg] = self.kwargs.get(self.lookup_field) - filter_kwargs = { lookup_url_kwarg: self.kwargs[lookup_url_kwarg] } + filter_kwargs = {lookup_url_kwarg: self.kwargs[lookup_url_kwarg]} queryset = self.filter_queryset(self.get_queryset()) @@ -248,9 +278,9 @@ def get_removed_object(self, search_params=None): if not search_params: lookup_value = self.kwargs.get(self.lookup_field) if CS.is_primary_key(lookup_value): - search_params = { 'pk': lookup_value } - elif hasattr(self, 'lookup_field_other'): - search_params = { self.lookup_field_other: lookup_value } + search_params = {"pk": lookup_value} + elif hasattr(self, "lookup_field_other"): + search_params = {self.lookup_field_other: lookup_value} else: raise Http404 @@ -266,7 +296,7 @@ def update(self, request, *args, **kwargs): def update_bulk(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() - kwargs['context'] = self.get_serializer_context() + kwargs["context"] = self.get_serializer_context() results, http_status = CS.update_bulk(request, self.object, serializer_class, **kwargs) response = Response(results, status=http_status) self._check_and_store_bulk_error(request, response) @@ -274,14 +304,14 @@ def update_bulk(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs): CS.update_common_info(request) - kwargs['partial'] = True + kwargs["partial"] = True res = super(CommonViewSet, self).update(request, *args, **kwargs) return res def partial_update_bulk(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() - kwargs['context'] = self.get_serializer_context() - kwargs['partial'] = True + kwargs["context"] = self.get_serializer_context() + kwargs["partial"] = True results, http_status = CS.update_bulk(request, self.object, serializer_class, **kwargs) response = Response(results, status=http_status) self._check_and_store_bulk_error(request, response) @@ -289,7 +319,7 @@ def partial_update_bulk(self, request, *args, **kwargs): def create(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() - kwargs['context'] = self.get_serializer_context() + kwargs["context"] = self.get_serializer_context() results, http_status = self.create_bulk_method(request, serializer_class, **kwargs) response = Response(results, status=http_status) self._check_and_store_bulk_error(request, response) @@ -308,9 +338,9 @@ def initialize_request(self, request, *args, **kwargs): Overrided from rest_framework to preserve the username and other variables set during identifyapicaller middleware. """ - username = request.user.username if hasattr(request.user, 'username') else None - is_service = request.user.is_service if hasattr(request.user, 'is_service') else False - token = request.user.token if hasattr(request.user, 'token') else None + username = request.user.username if hasattr(request.user, "username") else None + is_service = request.user.is_service if hasattr(request.user, "is_service") else False + token = request.user.token if hasattr(request.user, "token") else None drf_req = super(CommonViewSet, self).initialize_request(request, *args, **kwargs) @@ -330,8 +360,10 @@ def set_json_schema(self, view_file): always looks for the schema from a directory relative to the view's location, taking into account its version. """ - self.json_schema = CS.get_json_schema(path.dirname(view_file) + '/../schemas', - self.__class__.__name__.lower()[:-(len('viewset'))]) + self.json_schema = CS.get_json_schema( + path.dirname(view_file) + "/../schemas", + self.__class__.__name__.lower()[: -(len("viewset"))], + ) def _check_and_store_bulk_error(self, request, response): """ @@ -339,8 +371,13 @@ def _check_and_store_bulk_error(self, request, response): error data is not saved. Separately check presence of failures in bulk operations responses, and save data if necessary. """ - if 'failed' in response.data and len(response.data['failed']): - ApiErrorService.store_error_details(request, response, other={ 'bulk_request': True }) + if "failed" in response.data and len(response.data["failed"]): + try: + error_json = ApiErrorSerializerV2.request_to_json(self.request, response, other={"bulk_request": True}) + response.data["error_identifier"] = error_json["identifier"] + rabbitmq.publish(error_json, exchange="apierrors") + except Exception as e: + _logger.error(f"could not send api error to rabbitmq. Error: {e}") def get_api_name(self): """ @@ -349,4 +386,4 @@ def get_api_name(self): (for example, directories-api), will inherit this and return a customized result. """ - return '%ss' % self.__class__.__name__.split('ViewSet')[0].lower() + return "%ss" % self.__class__.__name__.split("ViewSet")[0].lower() diff --git a/src/metax_api/api/rest/base/views/contract_view.py b/src/metax_api/api/rest/base/views/contract_view.py index 39c9841d..f3ced7ec 100755 --- a/src/metax_api/api/rest/base/views/contract_view.py +++ b/src/metax_api/api/rest/base/views/contract_view.py @@ -21,7 +21,7 @@ class ContractViewSet(CommonViewSet): serializer_class = ContractSerializer object = Contract - lookup_field = 'pk' + lookup_field = "pk" def __init__(self, *args, **kwargs): self.set_json_schema(__file__) @@ -31,7 +31,7 @@ def get_object(self): lookup_value = self.kwargs.get(self.lookup_field, False) search_params = None if not CommonService.is_primary_key(lookup_value): - search_params = { 'contract_json__contains': { 'identifier': lookup_value }} + search_params = {"contract_json__contains": {"identifier": lookup_value}} return super(ContractViewSet, self).get_object(search_params=search_params) def get_queryset(self): @@ -40,13 +40,14 @@ def get_queryset(self): else: query_params = self.request.query_params additional_filters = {} - if query_params.get('organization', False): - additional_filters['contract_json__contains'] = { - 'organization': { 'organization_identifier': query_params['organization'] }} + if query_params.get("organization", False): + additional_filters["contract_json__contains"] = { + "organization": {"organization_identifier": query_params["organization"]} + } return super(ContractViewSet, self).get_queryset().filter(**additional_filters) - @action(detail=True, methods=['get'], url_path="datasets") + @action(detail=True, methods=["get"], url_path="datasets") def datasets_get(self, request, pk=None): contract = self.get_object() - catalog_records = [ CatalogRecordSerializer(f).data for f in contract.records.all() ] + catalog_records = [CatalogRecordSerializer(f).data for f in contract.records.all()] return Response(data=catalog_records, status=status.HTTP_200_OK) diff --git a/src/metax_api/api/rest/base/views/data_catalog_view.py b/src/metax_api/api/rest/base/views/data_catalog_view.py index 9ee1b692..b9bb09ff 100755 --- a/src/metax_api/api/rest/base/views/data_catalog_view.py +++ b/src/metax_api/api/rest/base/views/data_catalog_view.py @@ -17,7 +17,7 @@ class DataCatalogViewSet(CommonViewSet): serializer_class = DataCatalogSerializer object = DataCatalog - lookup_field = 'pk' + lookup_field = "pk" def __init__(self, *args, **kwargs): self.set_json_schema(__file__) @@ -41,7 +41,7 @@ def _search_using_other_data_catalog_identifiers(self): lookup_value = self.kwargs.get(self.lookup_field) try: - obj = self._search_from_catalog_json({'identifier': lookup_value}, True) + obj = self._search_from_catalog_json({"identifier": lookup_value}, True) except Exception: raise @@ -50,7 +50,8 @@ def _search_using_other_data_catalog_identifiers(self): def _search_from_catalog_json(self, search_json, raise_on_404): try: return super(DataCatalogViewSet, self).get_object( - search_params={'catalog_json__contains': search_json}) + search_params={"catalog_json__contains": search_json} + ) except Http404: if raise_on_404: raise diff --git a/src/metax_api/api/rest/base/views/dataset_view.py b/src/metax_api/api/rest/base/views/dataset_view.py index d18becd1..bd416afa 100755 --- a/src/metax_api/api/rest/base/views/dataset_view.py +++ b/src/metax_api/api/rest/base/views/dataset_view.py @@ -17,7 +17,11 @@ from metax_api.exceptions import Http400, Http403 from metax_api.models import CatalogRecord, Common, DataCatalog, Directory, File from metax_api.renderers import XMLRenderer -from metax_api.services import CatalogRecordService, CommonService as CS, RabbitMQService as rabbitmq +from metax_api.services import ( + CatalogRecordService, + CommonService as CS, + RabbitMQService as rabbitmq, +) from metax_api.settings import env from ..serializers import CatalogRecordSerializer, LightFileSerializer @@ -52,9 +56,7 @@ def get_object(self): try: cr = super(DatasetViewSet, self).get_object() except Http404: - if self.service_class.is_primary_key( - self.kwargs.get(self.lookup_field, False) - ): + if self.service_class.is_primary_key(self.kwargs.get(self.lookup_field, False)): # fail on pk search is clear... raise @@ -66,7 +68,7 @@ def get_object(self): return cr def get_queryset(self): - if not CS.get_boolean_query_param(self.request, 'include_legacy'): + if not CS.get_boolean_query_param(self.request, "include_legacy"): self.queryset = self.queryset.exclude( data_catalog__catalog_json__identifier__in=settings.LEGACY_CATALOGS ) @@ -105,9 +107,7 @@ def list(self, request, *args, **kwargs): # best to specify a variable for parameters intended for filtering purposes in get_queryset(), # because other api's may use query parameters of the same name, which can # mess up filtering if get_queryset() uses request.query_parameters directly. - self.queryset_search_params = self.service_class.get_queryset_search_params( - request - ) + self.queryset_search_params = self.service_class.get_queryset_search_params(request) if "preferred_identifier" in request.query_params: return self._retrieve_by_preferred_identifier(request, *args, **kwargs) @@ -134,14 +134,10 @@ def _metadata_version_get(self, request, *args, **kwargs): if self.service_class.is_primary_key(kwargs["metadata_version_identifier"]): search_params["id"] = kwargs["metadata_version_identifier"] else: - search_params["metadata_version_identifier"] = kwargs[ - "metadata_version_identifier" - ] + search_params["metadata_version_identifier"] = kwargs["metadata_version_identifier"] try: - research_dataset = cr.research_dataset_versions.get( - **search_params - ).research_dataset + research_dataset = cr.research_dataset_versions.get(**search_params).research_dataset except: raise Http404 @@ -152,10 +148,8 @@ def _metadata_version_get(self, request, *args, **kwargs): # possible to use the serializer, since an older metadata version of a ds # is not stored as part of the cr, but in the table ResearchDatasetVersion. # therefore, perform this checking and stripping separately here. - research_dataset = ( - self.service_class.check_and_remove_metadata_based_on_access_type( - self.service_class.remove_contact_info_metadata(research_dataset) - ) + research_dataset = self.service_class.check_and_remove_metadata_based_on_access_type( + self.service_class.remove_contact_info_metadata(research_dataset) ) return Response(data=research_dataset, status=status.HTTP_200_OK) @@ -207,9 +201,7 @@ def files_list(self, request, pk=None): @action(detail=False, methods=["get"], url_path="identifiers") def get_all_identifiers(self, request): - self.queryset_search_params = self.service_class.get_queryset_search_params( - request - ) + self.queryset_search_params = self.service_class.get_queryset_search_params(request) q = self.get_queryset().values("identifier") identifiers = [item["identifier"] for item in q] return Response(identifiers) @@ -217,26 +209,18 @@ def get_all_identifiers(self, request): @action(detail=False, methods=["get"], url_path="metadata_version_identifiers") def get_all_metadata_version_identifiers(self, request): # todo probably remove at some point - self.queryset_search_params = self.service_class.get_queryset_search_params( - request - ) + self.queryset_search_params = self.service_class.get_queryset_search_params(request) q = self.get_queryset().values("research_dataset") - identifiers = [ - item["research_dataset"]["metadata_version_identifier"] for item in q - ] + identifiers = [item["research_dataset"]["metadata_version_identifier"] for item in q] return Response(identifiers) @action(detail=False, methods=["get"], url_path="unique_preferred_identifiers") def get_all_unique_preferred_identifiers(self, request): - self.queryset_search_params = self.service_class.get_queryset_search_params( - request - ) + self.queryset_search_params = self.service_class.get_queryset_search_params(request) if CS.get_boolean_query_param(request, "latest"): queryset = ( - self.get_queryset() - .filter(next_dataset_version_id=None) - .values("research_dataset") + self.get_queryset().filter(next_dataset_version_id=None).values("research_dataset") ) else: queryset = self.get_queryset().values("research_dataset") @@ -254,19 +238,14 @@ def _search_using_dataset_identifiers(self): """ lookup_value = self.kwargs.get(self.lookup_field, False) - if ( - "preferred_identifier" in self.request.query_params - and self.request.method == "GET" - ): + if "preferred_identifier" in self.request.query_params and self.request.method == "GET": # search by preferred_identifier only for GET requests, while preferring: # - hits from att catalogs (assumed to be first created. improve logic if situation changes) # - first created (the first harvested occurrence, probably) # note: cant use get_object(), because get_object() will throw an error if there are multiple results obj = ( self.get_queryset() - .filter( - research_dataset__contains={"preferred_identifier": lookup_value} - ) + .filter(research_dataset__contains={"preferred_identifier": lookup_value}) .order_by("data_catalog_id", "date_created") .first() ) @@ -280,17 +259,13 @@ def _search_using_dataset_identifiers(self): # services using this... return super(DatasetViewSet, self).get_object( search_params={ - "research_dataset__contains": { - "metadata_version_identifier": lookup_value - } + "research_dataset__contains": {"metadata_version_identifier": lookup_value} } ) except Http404: pass - return super(DatasetViewSet, self).get_object( - search_params={"identifier": lookup_value} - ) + return super(DatasetViewSet, self).get_object(search_params={"identifier": lookup_value}) @action(detail=True, methods=["get"], url_path="redis") def redis_test(self, request, pk=None): # pragma: no cover @@ -321,12 +296,8 @@ def redis_test(self, request, pk=None): # pragma: no cover def rabbitmq_test(self, request, pk=None): # pragma: no cover if request.user.username != "metax": raise Http403() - rabbitmq.publish( - {"msg": "hello create"}, routing_key="create", exchange="datasets" - ) - rabbitmq.publish( - {"msg": "hello update"}, routing_key="update", exchange="datasets" - ) + rabbitmq.publish({"msg": "hello create"}, routing_key="create", exchange="datasets") + rabbitmq.publish({"msg": "hello update"}, routing_key="update", exchange="datasets") return Response(data={}, status=status.HTTP_200_OK) @action( @@ -351,9 +322,7 @@ def update_cr_total_files_byte_sizes(self, request): # Update IDA CR total_files_byte_size field value without creating a new version # Skip CatalogRecord save since it prohibits changing the value of total_files_byte_size for cr in self.object.objects.filter(data_catalog_id__in=ida_catalog_ids): - cr.research_dataset["total_files_byte_size"] = sum( - f.byte_size for f in cr.files.all() - ) + cr.research_dataset["total_files_byte_size"] = sum(f.byte_size for f in cr.files.all()) cr.preserve_version = True super(Common, cr).save() @@ -411,8 +380,8 @@ def list_datasets(self, request): def destroy_bulk(self, request, *args, **kwargs): return self.service_class.destroy_bulk(request) - @action(detail=False, methods=['post'], url_path="flush_password") - def flush_password(self, request): # pragma: no cover + @action(detail=False, methods=["post"], url_path="flush_password") + def flush_password(self, request): # pragma: no cover """ Set a password for flush api """ diff --git a/src/metax_api/api/rest/base/views/directory_view.py b/src/metax_api/api/rest/base/views/directory_view.py index 53ebf0b3..587363c2 100755 --- a/src/metax_api/api/rest/base/views/directory_view.py +++ b/src/metax_api/api/rest/base/views/directory_view.py @@ -22,14 +22,14 @@ class DirectoryViewSet(CommonViewSet): serializer_class = DirectorySerializer object = Directory - select_related = ['parent_directory'] - lookup_field_other = 'identifier' + select_related = ["parent_directory"] + lookup_field_other = "identifier" def get_api_name(self): """ Overrided due to not being able to follow common plural pattern... """ - return 'directories' + return "directories" def list(self, request, *args, **kwargs): raise Http501() @@ -57,31 +57,36 @@ def _get_directory_contents(self, request, identifier=None): A wrapper to call FS to collect and validate parameters from the request, and then call FS.get_directory_contents(). """ - paginate = CommonService.get_boolean_query_param(request, 'pagination') - path = request.query_params.get('path', None) - include_parent = CommonService.get_boolean_query_param(request, 'include_parent') - dirs_only = CommonService.get_boolean_query_param(request, 'directories_only') - recursive = CommonService.get_boolean_query_param(request, 'recursive') - max_depth = request.query_params.get('depth', 1) - project_identifier = request.query_params.get('project', None) - cr_identifier = request.query_params.get('cr_identifier', None) - not_cr_identifier = request.query_params.get('not_cr_identifier', None) - file_name = request.query_params.get('file_name') - directory_name = request.query_params.get('directory_name') + paginate = CommonService.get_boolean_query_param(request, "pagination") + path = request.query_params.get("path", None) + include_parent = CommonService.get_boolean_query_param(request, "include_parent") + dirs_only = CommonService.get_boolean_query_param(request, "directories_only") + recursive = CommonService.get_boolean_query_param(request, "recursive") + max_depth = request.query_params.get("depth", 1) + project_identifier = request.query_params.get("project", None) + cr_identifier = request.query_params.get("cr_identifier", None) + not_cr_identifier = request.query_params.get("not_cr_identifier", None) + file_name = request.query_params.get("file_name") + directory_name = request.query_params.get("directory_name") # max_depth can be an integer > 0, or * for everything. try: max_depth = int(max_depth) except ValueError: - if max_depth != '*': - raise Http400({ 'detail': ['value of depth must be an integer higher than 0, or *'] }) + if max_depth != "*": + raise Http400({"detail": ["value of depth must be an integer higher than 0, or *"]}) else: if max_depth <= 0: - raise Http400({ 'detail': ['value of depth must be higher than 0'] }) + raise Http400({"detail": ["value of depth must be higher than 0"]}) if cr_identifier and not_cr_identifier: - raise Http400({ 'detail': - ["there can only be one query parameter of 'cr_identifier' and 'not_cr_identifier'"] }) + raise Http400( + { + "detail": [ + "there can only be one query parameter of 'cr_identifier' and 'not_cr_identifier'" + ] + } + ) files_and_dirs = FileService.get_directory_contents( identifier=identifier, @@ -96,7 +101,7 @@ def _get_directory_contents(self, request, identifier=None): file_name=file_name, directory_name=directory_name, paginate=paginate, - request=request + request=request, ) if paginate: @@ -104,14 +109,14 @@ def _get_directory_contents(self, request, identifier=None): return Response(files_and_dirs) - @action(detail=True, methods=['get'], url_path="files") + @action(detail=True, methods=["get"], url_path="files") def get_files(self, request, pk=None): """ Return a list of child files and directories of a directory. """ return self._get_directory_contents(request, identifier=pk) - @action(detail=False, methods=['get'], url_path="files") + @action(detail=False, methods=["get"], url_path="files") def get_files_by_path(self, request): """ Return a list of child files and directories of a directory, queried @@ -119,17 +124,17 @@ def get_files_by_path(self, request): """ errors = defaultdict(list) - if 'project' not in request.query_params: - errors['detail'].append('project is a required query parameter') - if 'path' not in request.query_params: - errors['detail'].append('path is a required query parameter') + if "project" not in request.query_params: + errors["detail"].append("project is a required query parameter") + if "path" not in request.query_params: + errors["detail"].append("path is a required query parameter") if errors: raise Http400(errors) return self._get_directory_contents(request) - @action(detail=False, methods=['get'], url_path="root") + @action(detail=False, methods=["get"], url_path="root") def get_project_root_directory(self, request): """ Return root directory for a project. This is useful when starting @@ -138,18 +143,18 @@ def get_project_root_directory(self, request): Example: GET /directories/root?project=projext_x """ - if 'project' not in request.query_params: - raise Http400('project is a required query parameter') + if "project" not in request.query_params: + raise Http400("project is a required query parameter") if not request.user.is_service: - FileService.check_user_belongs_to_project(request, request.query_params['project']) + FileService.check_user_belongs_to_project(request, request.query_params["project"]) - root_dirs = FileService.get_project_root_directory(request.query_params['project']) + root_dirs = FileService.get_project_root_directory(request.query_params["project"]) return Response(root_dirs) - @action(detail=False, methods=['get'], url_path="update_byte_sizes_and_file_counts") - def update_byte_sizes_and_file_counts(self, request): # pragma: no cover + @action(detail=False, methods=["get"], url_path="update_byte_sizes_and_file_counts") + def update_byte_sizes_and_file_counts(self, request): # pragma: no cover """ Calculate byte sizes and file counts for all dirs in all projects. Intended to be called after importing test data. @@ -157,10 +162,14 @@ def update_byte_sizes_and_file_counts(self, request): # pragma: no cover If needed there should be no harm in calling this method again at any time in an attempt to correct mistakes in real data. """ - if request.user.username != 'metax': + if request.user.username != "metax": raise Http403 - for p in Directory.objects.all().distinct('project_identifier').values_list('project_identifier', flat=True): + for p in ( + Directory.objects.all() + .distinct("project_identifier") + .values_list("project_identifier", flat=True) + ): FileService.calculate_project_directory_byte_sizes_and_file_counts(p) return Response() diff --git a/src/metax_api/api/rest/base/views/file_storage_view.py b/src/metax_api/api/rest/base/views/file_storage_view.py index ff2b57c8..c2fcf663 100755 --- a/src/metax_api/api/rest/base/views/file_storage_view.py +++ b/src/metax_api/api/rest/base/views/file_storage_view.py @@ -20,7 +20,7 @@ class FileStorageViewSet(CommonViewSet): serializer_class = FileStorageSerializer object = FileStorage - lookup_field = 'pk' + lookup_field = "pk" def __init__(self, *args, **kwargs): self.set_json_schema(__file__) @@ -35,7 +35,8 @@ def get_object(self): # was not a pk - try identifier lookup_value = self.kwargs.pop(self.lookup_field) return super(FileStorageViewSet, self).get_object( - search_params={'file_storage_json__contains': {'identifier': lookup_value}}) + search_params={"file_storage_json__contains": {"identifier": lookup_value}} + ) def set_json_schema(self, view_file): - self.json_schema = CS.get_json_schema(path.dirname(view_file) + '/../schemas', 'file') + self.json_schema = CS.get_json_schema(path.dirname(view_file) + "/../schemas", "file") diff --git a/src/metax_api/api/rest/base/views/file_view.py b/src/metax_api/api/rest/base/views/file_view.py index d093b241..32e3b891 100755 --- a/src/metax_api/api/rest/base/views/file_view.py +++ b/src/metax_api/api/rest/base/views/file_view.py @@ -29,7 +29,7 @@ # i.e. /rest/v6/files, but must NOT end in / # or: /rest/files, but must NOT end in / -RE_PATTERN_FILES_CREATE = re.compile(r'^/rest/(v\d/)?files(?!/)') +RE_PATTERN_FILES_CREATE = re.compile(r"^/rest/(v\d/)?files(?!/)") # none of the methods in this class use atomic requests by default! see method dispatch() @@ -38,12 +38,12 @@ class FileViewSet(CommonViewSet): serializer_class = FileSerializer object = File - select_related = ['file_storage', 'parent_directory'] + select_related = ["file_storage", "parent_directory"] - lookup_field = 'pk' + lookup_field = "pk" # allow search by external identifier (urn, or whatever string in practice) as well - lookup_field_other = 'identifier' + lookup_field_other = "identifier" # customized create_bulk which handles both directories and files in the same # bulk_create request. @@ -55,9 +55,9 @@ def __init__(self, *args, **kwargs): def _use_transaction(self, request): # todo add checking of ?atomic parameter too? - if CommonService.get_boolean_query_param(self.request, 'dryrun'): + if CommonService.get_boolean_query_param(self.request, "dryrun"): return True - elif request.method == 'POST' and RE_PATTERN_FILES_CREATE.match(request.META['PATH_INFO']): + elif request.method == "POST" and RE_PATTERN_FILES_CREATE.match(request.META["PATH_INFO"]): # for POST /files only (creating), do not use a transaction ! return False return True @@ -78,10 +78,10 @@ def dispatch(self, request, **kwargs): """ if self._use_transaction(request): with transaction.atomic(): - _logger.debug('Note: Request in transaction') + _logger.debug("Note: Request in transaction") return super().dispatch(request, **kwargs) else: - _logger.debug('Note: Request not in transaction') + _logger.debug("Note: Request not in transaction") return super().dispatch(request, **kwargs) def list(self, request, *args, **kwargs): @@ -89,20 +89,20 @@ def list(self, request, *args, **kwargs): if not request.user.is_service: # end users can only retrieve their own files user_projects = AuthService.get_user_projects(request) - self.queryset_search_params['project_identifier__in'] = user_projects + self.queryset_search_params["project_identifier__in"] = user_projects return super().list(request, *args, **kwargs) def update(self, request, *args, **kwargs): - #This have to be checked before updating common info + # This have to be checked before updating common info if not isinstance(self.request.data, dict): - raise Http400('request message body must be a single json object') + raise Http400("request message body must be a single json object") return super().update(request, *args, **kwargs) def partial_update(self, request, *args, **kwargs): - #This have to be checked before updating common info + # This have to be checked before updating common info if not isinstance(self.request.data, dict): - raise Http400('request message body must be a single json object') + raise Http400("request message body must be a single json object") return super().partial_update(request, *args, **kwargs) @@ -115,15 +115,15 @@ def get_queryset(self): checksum:value --> checksum_value checksum --> checksum_algorithm/checked/value """ - if 'fields' in self.request.query_params: + if "fields" in self.request.query_params: self.fields = [] - for field in self.request.query_params['fields'].split(','): - field_names = field.split(':') - if 'checksum' == field_names[0] and len(field_names) > 1: - self.fields.append(f'{field_names[0]}_{field_names[1]}') + for field in self.request.query_params["fields"].split(","): + field_names = field.split(":") + if "checksum" == field_names[0] and len(field_names) > 1: + self.fields.append(f"{field_names[0]}_{field_names[1]}") - elif 'checksum' == field_names[0]: - self.fields.extend([f'checksum_{k}' for k in FileSerializer.checksum_fields]) + elif "checksum" == field_names[0]: + self.fields.extend([f"checksum_{k}" for k in FileSerializer.checksum_fields]) else: self.fields.append(field_names[0]) @@ -137,9 +137,9 @@ def get_object(self, search_params=None): """ obj = super().get_object(search_params) if self.request.user.is_service: - allowed_projects = CommonService.get_list_query_param(self.request, 'allowed_projects') + allowed_projects = CommonService.get_list_query_param(self.request, "allowed_projects") if allowed_projects is not None and obj.project_identifier not in allowed_projects: - raise Http403('You do not have permission to update this file') + raise Http403("You do not have permission to update this file") return obj @@ -163,7 +163,7 @@ def partial_update_bulk(self, request, *args, **kwargs): return super().partial_update_bulk(request, *args, **kwargs) - @action(detail=False, methods=['post'], url_path="datasets") + @action(detail=False, methods=["post"], url_path="datasets") def datasets(self, request): """ keys=files: Find out which datasets a list of files belongs to, and return their @@ -180,24 +180,26 @@ def datasets(self, request): requests to query parameters, so using POST instead is more guaranteed to work. """ - keysonly = CommonService.get_boolean_query_param(request, 'keysonly') - detailed = CommonService.get_boolean_query_param(request, 'detailed') + keysonly = CommonService.get_boolean_query_param(request, "keysonly") + detailed = CommonService.get_boolean_query_param(request, "detailed") params = request.query_params if not params.keys(): - return FileService.get_identifiers(request.data, 'noparams', True) + return FileService.get_identifiers(request.data, "noparams", True) - if 'keys' in params.keys(): - if params['keys'] in ['files', 'datasets']: - return FileService.get_identifiers(request.data, params['keys'], keysonly) + if "keys" in params.keys(): + if params["keys"] in ["files", "datasets"]: + return FileService.get_identifiers(request.data, params["keys"], keysonly) - if detailed: # This can be removed as soon as front can listen to ?keys=files which returns the same - return FileService.get_identifiers(request.data, 'files', False) + if ( + detailed + ): # This can be removed as soon as front can listen to ?keys=files which returns the same + return FileService.get_identifiers(request.data, "files", False) - raise Http403({ 'detail': [ 'Invalid parameters' ]}) + raise Http403({"detail": ["Invalid parameters"]}) - @action(detail=False, methods=['post'], url_path="restore") + @action(detail=False, methods=["post"], url_path="restore") def restore_files(self, request): """ Restore removed files. @@ -210,30 +212,30 @@ def destroy(self, request, pk, **kwargs): def destroy_bulk(self, request, *args, **kwargs): return FileService.destroy_bulk(request.data) - @action(detail=True, methods=['get', 'post', 'put', 'delete'], url_path='xml') + @action(detail=True, methods=["get", "post", "put", "delete"], url_path="xml") def xml_handler(self, request, pk=None): file = self.get_object() - if request.method == 'GET': + if request.method == "GET": return self._get_xml(request, file) else: - if 'namespace' not in request.query_params: - raise Http400('namespace is a required query parameter') + if "namespace" not in request.query_params: + raise Http400("namespace is a required query parameter") - if request.method == 'PUT': + if request.method == "PUT": return self._update_xml(request, file) - elif request.method == 'POST': + elif request.method == "POST": return self._create_xml(request, file) - elif request.method == 'DELETE': + elif request.method == "DELETE": return self._delete_xml(request, file) else: raise Http404 def _get_xml(self, request, file): - if 'namespace' in request.query_params: + if "namespace" in request.query_params: # get single requested xml metadata by namespace try: - xml_metadata = file.xmlmetadata_set.get(namespace=request.query_params['namespace']) + xml_metadata = file.xmlmetadata_set.get(namespace=request.query_params["namespace"]) except XmlMetadata.DoesNotExist: raise Http404 request.accepted_renderer = XMLRenderer() @@ -241,18 +243,20 @@ def _get_xml(self, request, file): else: # return list of namespaces of xml metadatas associated with the file - xml_namespaces = file.xmlmetadata_set.all().values_list('namespace', flat=True) + xml_namespaces = file.xmlmetadata_set.all().values_list("namespace", flat=True) request.accepted_renderer = JSONRenderer() - return Response(data=[ ns for ns in xml_namespaces ], status=status.HTTP_200_OK) + return Response(data=[ns for ns in xml_namespaces], status=status.HTTP_200_OK) def _create_xml(self, request, file): try: - file.xmlmetadata_set.get(namespace=request.query_params['namespace']) + file.xmlmetadata_set.get(namespace=request.query_params["namespace"]) except XmlMetadata.DoesNotExist: # good - create for the first time pass else: - raise Http400('xml metadata with namespace %s already exists' % request.query_params['namespace']) + raise Http400( + "xml metadata with namespace %s already exists" % request.query_params["namespace"] + ) new_xml_metadata = self._xml_request_to_dict_data(request, file) serializer = XmlMetadataSerializer(data=new_xml_metadata) @@ -268,7 +272,7 @@ def _create_xml(self, request, file): def _update_xml(self, request, file): try: - xml_metadata = file.xmlmetadata_set.get(namespace=request.query_params['namespace']) + xml_metadata = file.xmlmetadata_set.get(namespace=request.query_params["namespace"]) except XmlMetadata.DoesNotExist: raise Http404 @@ -285,7 +289,7 @@ def _update_xml(self, request, file): def _delete_xml(self, request, file): try: - xml_metadata = file.xmlmetadata_set.get(namespace=request.query_params['namespace']) + xml_metadata = file.xmlmetadata_set.get(namespace=request.query_params["namespace"]) except XmlMetadata.DoesNotExist: raise Http404 xml_metadata.delete() @@ -298,14 +302,16 @@ def _xml_request_to_dict_data(self, request, file): """ common_info = CommonService.update_common_info(request, return_only=True) new_xml_metadata = { - 'file': file.id, - 'xml': request.data, - 'namespace': request.query_params['namespace'] + "file": file.id, + "xml": request.data, + "namespace": request.query_params["namespace"], } new_xml_metadata.update(common_info) return new_xml_metadata - @action(detail=False, methods=['post'], url_path="flush_project") - def flush_project(self, request): # pragma: no cover + @action(detail=False, methods=["post"], url_path="flush_project") + def flush_project(self, request): # pragma: no cover # todo remove api when comfortable - raise ValidationError({ 'detail': ['API has been moved to RPC API: /rpc/files/flush_project'] }) + raise ValidationError( + {"detail": ["API has been moved to RPC API: /rpc/files/flush_project"]} + ) diff --git a/src/metax_api/api/rest/base/views/schema_view.py b/src/metax_api/api/rest/base/views/schema_view.py index 7c480aa6..0afed1f7 100755 --- a/src/metax_api/api/rest/base/views/schema_view.py +++ b/src/metax_api/api/rest/base/views/schema_view.py @@ -16,13 +16,13 @@ class SchemaViewSet(viewsets.ReadOnlyModelViewSet): filter_backends = () authentication_classes = () permission_classes = (ServicePermissions,) - api_type = 'rest' + api_type = "rest" def list(self, request, *args, **kwargs): return SchemaService.get_all_schemas() def retrieve(self, request, *args, **kwargs): - return SchemaService.get_schema_content(kwargs.get('pk')) + return SchemaService.get_schema_content(kwargs.get("pk")) def get_queryset(self): return self.list(None) @@ -31,4 +31,4 @@ def get_api_name(self): """ Does not inherit from common... """ - return 'schemas' + return "schemas" diff --git a/src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json b/src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json index f2337862..b315e05e 100755 --- a/src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json +++ b/src/metax_api/api/rest/v2/api_schemas/ida_dataset_schema.json @@ -252,14 +252,6 @@ "type":"object", "$ref":"#/definitions/CatalogRecord" }, - "editor":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#editor", - "title":"Editor", - "description":"Software or service that is used to modify the catalog record and the dataset", - "@type":"@id", - "type":"object", - "$ref":"#/definitions/Editor" - }, "identifier":{ "@id":"http://uri.suomi.fi/datamodel/ns/mrd#identifier", "title":"Identifier", @@ -490,51 +482,6 @@ "identifier" ] }, - "Editor":{ - "title":"Editor", - "type":"object", - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#Editor", - "description":"Software or service that is used to modify the catalog record and the dataset.", - "minProperties":1, - "properties":{ - "identifier":{ - "@id":"http://purl.org/dc/terms/identifier", - "title":"Identifier", - "description":"Identifier of the editor, such as Qvain or harvester.", - "enum":[ - "QVAIN" - ], - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "owner_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#owner_identifier", - "title":"Owner identifier", - "description":"owner of the resource", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "creator_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#creator_identifier", - "title":"Creator identifier", - "description":"creator of the resource", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - }, - "record_id":{ - "@id":"http://uri.suomi.fi/datamodel/ns/mrd#recordIdentifier", - "title":"Record identifier", - "description":"local record identifier", - "@type":"http://www.w3.org/2001/XMLSchema#string", - "minLength":1, - "type":"string" - } - }, - "additionalProperties": true - }, "EntityRelation":{ "title":"Entity relation", "type":"object", diff --git a/src/metax_api/api/rest/v2/router.py b/src/metax_api/api/rest/v2/router.py index 97ed1999..46e703d5 100755 --- a/src/metax_api/api/rest/v2/router.py +++ b/src/metax_api/api/rest/v2/router.py @@ -24,19 +24,18 @@ from metax_api.api.rest.base import CustomRouter from metax_api.api.rest.base.views import ( - ApiErrorViewSet, ContractViewSet, DirectoryViewSet, FileStorageViewSet, FileViewSet, SchemaViewSet, ) +from metax_api.api.rest.v2.views import ApiErrorViewSetV2 from .views import DataCatalogViewSet, DatasetViewSet class CustomRouterV2(CustomRouter): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -48,61 +47,67 @@ def __init__(self, *args, **kwargs): # - retrieve all file user metadata # - update file user metadata in bulk-manner - self.routes.append(Route( - url=r'^{prefix}/{lookup}/files/user_metadata{trailing_slash}$', - mapping={ - 'get': 'files_user_metadata_list', - 'put': 'files_user_metadata_update', - 'patch': 'files_user_metadata_update', - }, - name='{basename}-files-user-metadata-list', - detail=True, - initkwargs={'suffix': 'FilesUserMetadataList'} - )) + self.routes.append( + Route( + url=r"^{prefix}/{lookup}/files/user_metadata{trailing_slash}$", + mapping={ + "get": "files_user_metadata_list", + "put": "files_user_metadata_update", + "patch": "files_user_metadata_update", + }, + name="{basename}-files-user-metadata-list", + detail=True, + initkwargs={"suffix": "FilesUserMetadataList"}, + ) + ) # - retrieve single dataset file technical metadata - self.routes.append(Route( - url=r'^{prefix}/{lookup}/files/(?P.+){trailing_slash}$', - mapping={ - 'get': 'files_retrieve', - }, - name='{basename}-files-retrieve', - detail=True, - initkwargs={'suffix': 'FilesRetrieve'} - )) + self.routes.append( + Route( + url=r"^{prefix}/{lookup}/files/(?P.+){trailing_slash}$", + mapping={ + "get": "files_retrieve", + }, + name="{basename}-files-retrieve", + detail=True, + initkwargs={"suffix": "FilesRetrieve"}, + ) + ) # - retrieve all dataset files technical metadata # - change files of a dataset - self.routes.append(Route( - url=r'^{prefix}/{lookup}/files{trailing_slash}$', - mapping={ - 'get': 'files_list', - 'post': 'files_post', - }, - name='{basename}-files-list', - detail=True, - initkwargs={'suffix': 'FilesList'} - )) + self.routes.append( + Route( + url=r"^{prefix}/{lookup}/files{trailing_slash}$", + mapping={ + "get": "files_list", + "post": "files_post", + }, + name="{basename}-files-list", + detail=True, + initkwargs={"suffix": "FilesList"}, + ) + ) # v2 urls, but using v1 view classes, because nothing changes router_v1 = CustomRouter(trailing_slash=False) -router_v1.register(r'apierrors/?', ApiErrorViewSet) -router_v1.register(r'contracts/?', ContractViewSet) -router_v1.register(r'directories/?', DirectoryViewSet) -router_v1.register(r'files/?', FileViewSet) -router_v1.register(r'filestorages/?', FileStorageViewSet) -router_v1.register(r'schemas/?', SchemaViewSet) +router_v1.register(r"contracts/?", ContractViewSet) +router_v1.register(r"directories/?", DirectoryViewSet) +router_v1.register(r"files/?", FileViewSet) +router_v1.register(r"filestorages/?", FileStorageViewSet) +router_v1.register(r"schemas/?", SchemaViewSet) # v2 urls, using v2 view classes with changes router_v2 = CustomRouterV2(trailing_slash=False) -router_v2.register(r'datasets/?', DatasetViewSet) -router_v2.register(r'datacatalogs/?', DataCatalogViewSet) +router_v2.register(r"apierrors/?", ApiErrorViewSetV2) +router_v2.register(r"datasets/?", DatasetViewSet) +router_v2.register(r"datacatalogs/?", DataCatalogViewSet) router_v2.register( - r'datasets/(?P.+)/metadata_versions/(?P.+)/?', - DatasetViewSet + r"datasets/(?P.+)/metadata_versions/(?P.+)/?", + DatasetViewSet, ) api_urlpatterns = router_v1.urls + router_v2.urls diff --git a/src/metax_api/api/rest/v2/serializers/__init__.py b/src/metax_api/api/rest/v2/serializers/__init__.py index da373ff1..0e40043a 100755 --- a/src/metax_api/api/rest/v2/serializers/__init__.py +++ b/src/metax_api/api/rest/v2/serializers/__init__.py @@ -5,5 +5,6 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +from .api_error_serializer import ApiErrorSerializerV2 from .catalog_record_serializer import CatalogRecordSerializerV2 from .data_catalog_serializer import DataCatalogSerializerV2 diff --git a/src/metax_api/api/rest/v2/serializers/api_error_serializer.py b/src/metax_api/api/rest/v2/serializers/api_error_serializer.py new file mode 100644 index 00000000..3e052674 --- /dev/null +++ b/src/metax_api/api/rest/v2/serializers/api_error_serializer.py @@ -0,0 +1,71 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT + +import traceback +from uuid import uuid4 + +from rest_framework.serializers import ModelSerializer + +from metax_api.models import ApiError +from metax_api.utils import get_tz_aware_now_without_micros + + +class ApiErrorSerializerV2(ModelSerializer): + class Meta: + model = ApiError + + fields = ( + "id", + "identifier", + "error", + "date_created" + ) + + @staticmethod + def request_to_json(request, response, other={}): + current_time = str(get_tz_aware_now_without_micros()).replace(" ", "T") + + if request.method in ("POST", "PUT", "PATCH"): + # cast possible datetime objects to strings, because those cant be json-serialized... + request_data = request.data + for date_field in ("date_modified", "date_created"): + if isinstance(request_data, list): + for item in request_data: + if isinstance(item, dict) and date_field in item: + item[date_field] = str(item[date_field]) + elif isinstance(request_data, dict) and date_field in request_data: + request_data[date_field] = str(request_data[date_field]) + else: + pass + else: + request_data = None + + error_info = { + "method": request.method, + "user": request.user.username or "guest", + "data": request_data, + "headers": { + k: v + for k, v in request.META.items() + if k.startswith("HTTP_") and k != "HTTP_AUTHORIZATION" + }, + "status_code": response.status_code, + "response": response.data, + "traceback": traceback.format_exc(), + # during test case execution, RAW_URI is not set + "url": request.META.get("RAW_URI", request.META.get("PATH_INFO", "???")), + "identifier": "%s-%s" % (current_time[:19], str(uuid4())[:8]), + "exception_time": current_time, + } + + if other: + # may contain info that the request was a bulk operation + error_info["other"] = {k: v for k, v in other.items()} + if "bulk_request" in other: + error_info["other"]["data_row_count"] = len(request_data) + + return error_info diff --git a/src/metax_api/api/rest/v2/serializers/catalog_record_serializer.py b/src/metax_api/api/rest/v2/serializers/catalog_record_serializer.py index d89dc29d..dd60e9af 100755 --- a/src/metax_api/api/rest/v2/serializers/catalog_record_serializer.py +++ b/src/metax_api/api/rest/v2/serializers/catalog_record_serializer.py @@ -16,13 +16,16 @@ from metax_api.api.rest.base.serializers import CatalogRecordSerializer from metax_api.api.rest.base.serializers.catalog_record_serializer import DFT_CATALOG from metax_api.models import CatalogRecordV2 -from metax_api.services import CatalogRecordService as CRS, CommonService as CS, RedisCacheService as cache +from metax_api.services import ( + CatalogRecordService as CRS, + CommonService as CS, + RedisCacheService as cache, +) _logger = logging.getLogger(__name__) class CatalogRecordSerializerV2(CatalogRecordSerializer): - class Meta: # deepcopied, so that changes in this model don't affect # the V1 model @@ -31,55 +34,56 @@ class Meta: # define separately for inherited class, so that schemas are searched # from api/rest/v2/schemas, instead of api/rest/v1/schemas - _schemas_directory_path = path.join(path.dirname(path.dirname(__file__)), 'schemas') + _schemas_directory_path = path.join(path.dirname(path.dirname(__file__)), "schemas") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.Meta.model = CatalogRecordV2 self.Meta.fields += ( - 'draft_of', - 'next_draft', + "draft_of", + "next_draft", + ) + self.Meta.extra_kwargs.update( + { + "draft_of": {"required": False}, + "next_draft": {"required": False}, + } ) - self.Meta.extra_kwargs.update({ - 'draft_of': { 'required': False }, - 'next_draft': { 'required': False }, - }) def is_valid(self, raise_exception=False): - if CS.get_boolean_query_param(self.context['request'], 'draft') and not self.initial_data.get('data_catalog'): - self.initial_data['data_catalog'] = DFT_CATALOG + if CS.get_boolean_query_param( + self.context["request"], "draft" + ) and not self.initial_data.get("data_catalog"): + self.initial_data["data_catalog"] = DFT_CATALOG - self.initial_data.pop('draft_of', None) - self.initial_data.pop('editor', None) - self.initial_data.pop('next_draft', None) + self.initial_data.pop("draft_of", None) + self.initial_data.pop("next_draft", None) super().is_valid(raise_exception=raise_exception) def to_representation(self, instance): res = super().to_representation(instance) - if 'request' in self.context: + if "request" in self.context: - if CS.get_boolean_query_param(self.context['request'], 'include_user_metadata'): + if CS.get_boolean_query_param(self.context["request"], "include_user_metadata"): # keep user metadata and possible file_details that have been populated in super().to_representation() pass else: - res.get('research_dataset', {}).pop('files', None) - res.get('research_dataset', {}).pop('directories', None) + res.get("research_dataset", {}).pop("files", None) + res.get("research_dataset", {}).pop("directories", None) - if 'draft_of' in res: - if instance.user_is_privileged(instance.request or self.context['request']): - res['draft_of'] = instance.draft_of.identifiers_dict + if "draft_of" in res: + if instance.user_is_privileged(instance.request or self.context["request"]): + res["draft_of"] = instance.draft_of.identifiers_dict else: - del res['draft_of'] + del res["draft_of"] - if 'next_draft' in res: - if instance.user_is_privileged(instance.request or self.context['request']): - res['next_draft'] = instance.next_draft.identifiers_dict + if "next_draft" in res: + if instance.user_is_privileged(instance.request or self.context["request"]): + res["next_draft"] = instance.next_draft.identifiers_dict else: - del res['next_draft'] - - res.pop('editor', None) + del res["next_draft"] return res @@ -97,15 +101,17 @@ def validate_research_dataset_files(self, value): CRS.validate_reference_data(value, cache) - rd_files_schema = CS.get_json_schema(self._schemas_directory_path, 'dataset_files') + rd_files_schema = CS.get_json_schema(self._schemas_directory_path, "dataset_files") resolver = RefResolver( # at some point when jsonschema package is updated, probably need to switch to # using the below commented out parameter names instead # schema_path='file:{}'.format(path.dirname(path.dirname(__file__)) + '/schemas/dataset_files_schema.json'), # schema=rd_files_schema - base_uri='file:{}'.format(path.join(self._schemas_directory_path, 'dataset_files_schema.json')), - referrer=rd_files_schema + base_uri="file:{}".format( + path.join(self._schemas_directory_path, "dataset_files_schema.json") + ), + referrer=rd_files_schema, ) # for debugging, below may be useful @@ -114,27 +120,35 @@ def validate_research_dataset_files(self, value): validator = Draft4Validator(rd_files_schema, resolver=resolver, format_checker=None) if not value: - _logger.info('Validating files and/or directories with empty value. Nothing to validate.') + _logger.info( + "Validating files and/or directories with empty value. Nothing to validate." + ) return try: validator.validate(value) except JsonValidationError as e: - raise ValidationError({ 'detail': - ['%s. Json path: %s. Schema: %s' % (e.message, [p for p in e.path], e.schema)] - }) + raise ValidationError( + { + "detail": [ + "%s. Json path: %s. Schema: %s" % (e.message, [p for p in e.path], e.schema) + ] + } + ) def _set_dataset_schema(self): if self._validate_as_draft(): # drafts only exists for V2 records, otherwise normal rules apply - schema_prefix = 'dft' - self.json_schema = CS.get_json_schema(self._schemas_directory_path, 'dataset', schema_prefix) + schema_prefix = "dft" + self.json_schema = CS.get_json_schema( + self._schemas_directory_path, "dataset", schema_prefix + ) else: super()._set_dataset_schema() def _validate_as_draft(self): if self._operation_is_create and "request" in self.context: - return CS.get_boolean_query_param(self.context["request"], 'draft') + return CS.get_boolean_query_param(self.context["request"], "draft") if CS.request_is_create_operation(self.instance.request): return self.instance._save_as_draft() diff --git a/src/metax_api/api/rest/v2/serializers/data_catalog_serializer.py b/src/metax_api/api/rest/v2/serializers/data_catalog_serializer.py index 717de238..e0d2799a 100755 --- a/src/metax_api/api/rest/v2/serializers/data_catalog_serializer.py +++ b/src/metax_api/api/rest/v2/serializers/data_catalog_serializer.py @@ -12,25 +12,31 @@ class DataCatalogSerializerV2(DataCatalogSerializer): - def is_valid(self, raise_exception=False): """ Define this here that data catalogs are validated using JSON-schemas from V2 directory. """ super(DataCatalogSerializer, self).is_valid(raise_exception=raise_exception) - if 'catalog_json' in self.initial_data: + if "catalog_json" in self.initial_data: self._validate_dataset_schema() - if self.initial_data['catalog_json'].get('dataset_versioning', False) is True \ - and self.initial_data['catalog_json'].get('harvested', False) is True: - raise ValidationError({ - 'detail': ['versioning cannot be enabled in harvested catalogs.'] - }) + if ( + self.initial_data["catalog_json"].get("dataset_versioning", False) is True + and self.initial_data["catalog_json"].get("harvested", False) is True + ): + raise ValidationError( + {"detail": ["versioning cannot be enabled in harvested catalogs."]} + ) def _validate_dataset_schema(self): - rd_schema = self.initial_data['catalog_json'].get('research_dataset_schema', None) + rd_schema = self.initial_data["catalog_json"].get("research_dataset_schema", None) if not rd_schema: return - schema_path = '%s/../schemas/%s_dataset_schema.json' % (path.dirname(__file__), rd_schema) + schema_path = "%s/../schemas/%s_dataset_schema.json" % ( + path.dirname(__file__), + rd_schema, + ) if not path.isfile(schema_path): - raise ValidationError({'catalog_json': ['research dataset schema \'%s\' not found' % rd_schema]}) + raise ValidationError( + {"catalog_json": ["research dataset schema '%s' not found" % rd_schema]} + ) diff --git a/src/metax_api/api/rest/v2/views/__init__.py b/src/metax_api/api/rest/v2/views/__init__.py index 112f422e..eeb06648 100755 --- a/src/metax_api/api/rest/v2/views/__init__.py +++ b/src/metax_api/api/rest/v2/views/__init__.py @@ -7,3 +7,4 @@ from .data_catalog_view import DataCatalogViewSet from .dataset_view import DatasetViewSet +from .api_error_view import ApiErrorViewSetV2 diff --git a/src/metax_api/api/rest/v2/views/api_error_view.py b/src/metax_api/api/rest/v2/views/api_error_view.py new file mode 100644 index 00000000..c0e477d4 --- /dev/null +++ b/src/metax_api/api/rest/v2/views/api_error_view.py @@ -0,0 +1,120 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT + +import logging + +from django.http import Http404 +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response + +from metax_api.api.rest.base.views import CommonViewSet +from metax_api.exceptions import Http403, Http501 +from metax_api.models import ApiError +from metax_api.permissions import ServicePermissions + +from ..serializers.api_error_serializer import ApiErrorSerializerV2 + +_logger = logging.getLogger(__name__) + +class ApiErrorViewSetV2(CommonViewSet): + + permission_classes = [ServicePermissions] + queryset = ApiError.objects.all() + serializer_class = ApiErrorSerializerV2 + + def initial(self, request, *args, **kwargs): + if request.user.username != "metax": + raise Http403 + return super().initial(request, *args, **kwargs) + + @action(detail=False, methods=["post"], url_path="flush") + def flush_errors(self, request): + """ + Delete all errors from database. + """ + _logger.info("%s called by %s" % (request.META["PATH_INFO"], request.user.username)) + errors = ApiError.objects.all() + errors_deleted_count = len(errors) + errors.delete() + return Response(data={"errors_deleted": errors_deleted_count}, status=200) + + def destroy(self, request, *args, **kwargs): + """ + Delete a single error from database. + """ + _logger.info("DELETE %s called by %s" % (request.META["PATH_INFO"], request.user.username)) + + pk = kwargs["pk"] + error: ApiError + try: + error = ApiError.objects.filter(pk=int(pk)).first() + except ValueError: + error = ApiError.objects.filter(identifier=pk).first() + + if error: + error.delete() + return Response(status=204) + else: + raise Http404 + + def retrieve(self, request, *args, **kwargs): + """ + Retrieve complete data about a single error. + """ + pk = kwargs["pk"] + error: ApiError + try: + error = ApiError.objects.filter(pk=int(pk)).first() + except ValueError: + error = ApiError.objects.filter(identifier=pk).first() + + if error: + serializer = ApiErrorSerializerV2(error) + return Response(data=serializer.data, status=200) + else: + raise Http404 + + def list(self, request, *args, **kwargs): + """ + List all errors. Data is cleaned up a bit for easier browsing. + """ + errors = ApiError.objects.all() + error_list = [] + for error in errors: + error_details = error.error + error_details.pop("data", None) + error_details.pop("headers", None) + if error_details.get("response") is not None: + if len(str(error_details["response"])) > 200: + print(error_details["response"]) + error_details["response"] = ( + "%s ...(first 200 characters)" % str(error_details["response"])[:200] + ) + print(error_details["response"]) + if error_details.get("traceback") is not None: + error_details["traceback"] = ( + "(last 200 characters) ...%s" % error_details["traceback"][-200:] + ) + error_list.append(error) + serializer = ApiErrorSerializerV2(error_list, many=True) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + def update(self, request, *args, **kwargs): + raise Http501() + + def update_bulk(self, request, *args, **kwargs): + raise Http501() + + def partial_update(self, request, *args, **kwargs): + raise Http501() + + def partial_update_bulk(self, request, *args, **kwargs): + raise Http501() + + def create(self, request, *args, **kwargs): + raise Http501() \ No newline at end of file diff --git a/src/metax_api/api/rest/v2/views/dataset_view.py b/src/metax_api/api/rest/v2/views/dataset_view.py index d9f7bf95..84eb1776 100755 --- a/src/metax_api/api/rest/v2/views/dataset_view.py +++ b/src/metax_api/api/rest/v2/views/dataset_view.py @@ -22,9 +22,10 @@ _logger = logging.getLogger(__name__) -UNAUTHORIZED_TO_SEE_FILES_MSG = \ - 'You do not have permission to see this information because the dataset access type ' \ - 'is not open and you are not the owner of the catalog record.' +UNAUTHORIZED_TO_SEE_FILES_MSG = ( + "You do not have permission to see this information because the dataset access type " + "is not open and you are not the owner of the catalog record." +) class DatasetViewSet(DatasetViewSet): @@ -33,7 +34,7 @@ class DatasetViewSet(DatasetViewSet): serializer_class = CatalogRecordSerializerV2 object = CatalogRecordV2 - @action(detail=True, methods=['get'], url_path="projects") + @action(detail=True, methods=["get"], url_path="projects") def projects_list(self, request, pk=None): # note: checks permissions @@ -43,7 +44,10 @@ def projects_list(self, request, pk=None): raise Http403 projects = [ - p for p in cr.files.all().values_list('project_identifier', flat=True).distinct('project_identifier') + p + for p in cr.files.all() + .values_list("project_identifier", flat=True) + .distinct("project_identifier") ] return Response(data=projects, status=status.HTTP_200_OK) @@ -53,7 +57,7 @@ def files_retrieve(self, request, pk=None, file_pk=None): """ Retrieve technical metadata of a single file associated with a dataset. """ - _logger.debug('Retrieving metadata of single file: %r' % file_pk) + _logger.debug("Retrieving metadata of single file: %r" % file_pk) # note: checks permissions cr = self.get_object() @@ -62,19 +66,19 @@ def files_retrieve(self, request, pk=None, file_pk=None): raise Http403(UNAUTHORIZED_TO_SEE_FILES_MSG) try: - params = { 'pk': int(file_pk) } + params = {"pk": int(file_pk)} except ValueError: - params = { 'identifier': file_pk } + params = {"identifier": file_pk} - manager = 'objects' + manager = "objects" - if CS.get_boolean_query_param(request, 'removed_files'): - params['removed'] = True - manager = 'objects_unfiltered' + if CS.get_boolean_query_param(request, "removed_files"): + params["removed"] = True + manager = "objects_unfiltered" file_fields = [] - if 'file_fields' in request.query_params: - file_fields = request.query_params['file_fields'].split(',') + if "file_fields" in request.query_params: + file_fields = request.query_params["file_fields"].split(",") file_fields = LightFileSerializer.ls_field_list(file_fields) @@ -96,7 +100,7 @@ def files_list(self, request, pk=None): NOTE! Take a look at api/rest/v2/router.py to see how this method is mapped to HTTP verb """ params = {} - manager = 'objects' + manager = "objects" # TODO: This applies only to IDA files, not remote resources. # TODO: Should this apply also to remote resources? cr = self.get_object() @@ -104,13 +108,13 @@ def files_list(self, request, pk=None): if not cr.authorized_to_see_catalog_record_files(request): raise Http403(UNAUTHORIZED_TO_SEE_FILES_MSG) - if CS.get_boolean_query_param(request, 'removed_files'): - params['removed'] = True - manager = 'objects_unfiltered' + if CS.get_boolean_query_param(request, "removed_files"): + params["removed"] = True + manager = "objects_unfiltered" file_fields = [] - if 'file_fields' in request.query_params: - file_fields = request.query_params['file_fields'].split(',') + if "file_fields" in request.query_params: + file_fields = request.query_params["file_fields"].split(",") file_fields = LightFileSerializer.ls_field_list(file_fields) queryset = cr.files(manager=manager).filter(**params).values(*file_fields) @@ -126,7 +130,7 @@ def files_post(self, request, pk=None): NOTE! Take a look at api/rest/v2/router.py to see how this method is mapped to HTTP verb """ if not request.data: - raise Http400('No data received') + raise Http400("No data received") # note: checks permissions cr = self.get_object() @@ -149,7 +153,7 @@ def files_user_metadata_list(self, request, pk=None): data = {} - for object_type in ('files', 'directories'): + for object_type in ("files", "directories"): if object_type in cr.research_dataset: data[object_type] = cr.research_dataset[object_type] @@ -163,7 +167,7 @@ def files_user_metadata_update(self, request, pk=None): This API does not add or remove files! Only updates metadata. """ if not request.data: - raise Http400('No data received') + raise Http400("No data received") # note: checks permissions cr = self.get_object() @@ -172,7 +176,11 @@ def files_user_metadata_update(self, request, pk=None): return Response(data=None, status=status.HTTP_200_OK) - @action(detail=True, methods=['get'], url_path="files/(?P.+)/user_metadata") + @action( + detail=True, + methods=["get"], + url_path="files/(?P.+)/user_metadata", + ) def files_user_metadata_retrieve(self, request, pk=None, obj_identifier=None): """ Retrieve user-provided dataset-specific metadata for a file or a directory associated with a dataset. @@ -184,14 +192,14 @@ def files_user_metadata_retrieve(self, request, pk=None, obj_identifier=None): if not cr.authorized_to_see_catalog_record_files(request): raise Http403(UNAUTHORIZED_TO_SEE_FILES_MSG) - if CS.get_boolean_query_param(request, 'directory'): + if CS.get_boolean_query_param(request, "directory"): # search from directories only if special query parameter is given - object_type = 'directories' + object_type = "directories" else: - object_type = 'files' + object_type = "files" for obj in cr.research_dataset.get(object_type, []): - if obj['identifier'] == obj_identifier: + if obj["identifier"] == obj_identifier: return Response(data=obj, status=status.HTTP_200_OK) raise Http404 diff --git a/src/metax_api/api/rpc/base/router.py b/src/metax_api/api/rpc/base/router.py index 85f69faf..1e799af0 100755 --- a/src/metax_api/api/rpc/base/router.py +++ b/src/metax_api/api/rpc/base/router.py @@ -27,19 +27,18 @@ class CustomRouter(DefaultRouter): - def get_default_basename(self, viewset): """ When a viewset has no queryset set, or base_name is not passed to a router as the 3rd parameter, automatically determine base name. """ - return viewset.__class__.__name__.split('RPC')[0] + return viewset.__class__.__name__.split("RPC")[0] router = CustomRouter(trailing_slash=False) -router.register(r'datasets/?', DatasetRPC) -router.register(r'files/?', FileRPC) -router.register(r'statistics/?', StatisticRPC) -router.register(r'elasticsearchs/?', ElasticsearchRPC) +router.register(r"datasets/?", DatasetRPC) +router.register(r"files/?", FileRPC) +router.register(r"statistics/?", StatisticRPC) +router.register(r"elasticsearchs/?", ElasticsearchRPC) api_urlpatterns = router.urls diff --git a/src/metax_api/api/rpc/base/views/common_rpc.py b/src/metax_api/api/rpc/base/views/common_rpc.py index 5f8f638b..f87a26e1 100755 --- a/src/metax_api/api/rpc/base/views/common_rpc.py +++ b/src/metax_api/api/rpc/base/views/common_rpc.py @@ -23,7 +23,7 @@ class CommonRPC(CommonViewSet): common tricks, such as saving errors to /apierrors, request modifications, permission objects... """ - api_type = 'rpc' + api_type = "rpc" # serves no purpose, but satisfies the ViewSet basic requirements serializer_class = FileSerializer @@ -37,7 +37,7 @@ def get_api_name(self): Some views where the below formula does not produce a sensible result will inherit this and return a customized result. """ - return '%ss' % self.__class__.__name__.split('RPC')[0].lower() + return "%ss" % self.__class__.__name__.split("RPC")[0].lower() def create(self, request, *args, **kwargs): raise Http501() diff --git a/src/metax_api/api/rpc/base/views/dataset_rpc.py b/src/metax_api/api/rpc/base/views/dataset_rpc.py index bda3a494..9a8f2336 100755 --- a/src/metax_api/api/rpc/base/views/dataset_rpc.py +++ b/src/metax_api/api/rpc/base/views/dataset_rpc.py @@ -18,7 +18,11 @@ from metax_api.exceptions import Http400, Http403 from metax_api.models import CatalogRecord from metax_api.models.catalog_record import DataciteDOIUpdate -from metax_api.services.datacite_service import DataciteException, DataciteService, convert_cr_to_datacite_cr_json +from metax_api.services.datacite_service import ( + DataciteException, + DataciteService, + convert_cr_to_datacite_cr_json, +) from metax_api.utils import generate_doi_identifier, is_metax_generated_doi_identifier from .common_rpc import CommonRPC @@ -31,71 +35,91 @@ class DatasetRPC(CommonRPC): serializer_class = CatalogRecordSerializer object = CatalogRecord - @action(detail=False, methods=['get'], url_path="get_minimal_dataset_template") + @action(detail=False, methods=["get"], url_path="get_minimal_dataset_template") def get_minimal_dataset_template(self, request): - if request.query_params.get('type', None) not in ['service', 'enduser']: - raise Http400({ - 'detail': ['query param \'type\' missing or wrong. please specify ?type= as one of: service, enduser'] - }) - - with open('metax_api/exampledata/dataset_minimal.json', 'rb') as f: + if request.query_params.get("type", None) not in ["service", "enduser", "service_pas", "enduser_pas"]: + raise Http400( + { + "detail": [ + "query param 'type' missing or wrong. please specify ?type= as one of: service, enduser, service_pas, enduser_pas" + ] + } + ) + + with open("metax_api/exampledata/dataset_minimal.json", "rb") as f: example_ds = load(f) - example_ds['data_catalog'] = django_settings.END_USER_ALLOWED_DATA_CATALOGS[0] + example_ds["data_catalog"] = django_settings.IDA_DATA_CATALOG_IDENTIFIER + + if request.query_params["type"].endswith("_pas"): + example_ds["data_catalog"] = django_settings.PAS_DATA_CATALOG_IDENTIFIER + + if 'issued' not in example_ds['research_dataset']: + example_ds['research_dataset']['issued'] = '2019-01-01' + if 'publisher' not in example_ds['research_dataset']: + example_ds['research_dataset']['publisher'] = example_ds['research_dataset']['creator'][0] - if request.query_params['type'] == 'enduser': - example_ds.pop('metadata_provider_org', None) - example_ds.pop('metadata_provider_user', None) + if request.query_params["type"].startswith("enduser"): + example_ds.pop("metadata_provider_org", None) + example_ds.pop("metadata_provider_user", None) return Response(example_ds) - @action(detail=False, methods=['post'], url_path="set_preservation_identifier") + @action(detail=False, methods=["post"], url_path="set_preservation_identifier") def set_preservation_identifier(self, request): - if not request.query_params.get('identifier', False): - raise Http400({ - 'detail': ['Query param \'identifier\' missing. Please specify ?identifier='] - }) + if not request.query_params.get("identifier", False): + raise Http400( + { + "detail": [ + "Query param 'identifier' missing. Please specify ?identifier=" + ] + } + ) try: - cr = self.object.objects.get(identifier=request.query_params['identifier']) + cr = self.object.objects.get(identifier=request.query_params["identifier"]) except self.object.DoesNotExist: raise Http404 if cr.preservation_identifier: # If cr preservation identifier already exists, make sure it also exists in Datacite - DataciteDOIUpdate(cr, cr.preservation_identifier, 'update')() + DataciteDOIUpdate(cr, cr.preservation_identifier, "update")() else: - pref_id = cr.research_dataset['preferred_identifier'] + pref_id = cr.research_dataset["preferred_identifier"] if is_metax_generated_doi_identifier(pref_id): # Copy metax generated doi identifier to preservation identifier. NOTE: This code should never be # reached since if pref id is a metax generated doi, it should have already been copied to preservation # identifier (ida dataset), but this is done just in case something has previously failed... - _logger.warning("Reached a code block in dataset_rpc set_preservation_identifier method, which should" - " not be reached.") + _logger.warning( + "Reached a code block in dataset_rpc set_preservation_identifier method, which should" + " not be reached." + ) cr.preservation_identifier = pref_id - action = 'update' + action = "update" else: # Generate a new DOI for the dataset. If pref id is a metax generated urn, use that urn's suffix as # the doi suffix. Otherwise generate completely new doi. - if pref_id.startswith('urn:nbn:fi:att:'): - cr.preservation_identifier = generate_doi_identifier(pref_id[len('urn:nbn:fi:att:'):]) + if pref_id.startswith("urn:nbn:fi:att:"): + cr.preservation_identifier = generate_doi_identifier( + pref_id[len("urn:nbn:fi:att:") :] + ) else: cr.preservation_identifier = generate_doi_identifier() - action = 'create' + action = "create" self._save_and_publish_dataset(cr, action) return Response(cr.preservation_identifier) - @action(detail=False, methods=['post'], url_path="change_cumulative_state") + @action(detail=False, methods=["post"], url_path="change_cumulative_state") def change_cumulative_state(self, request): - identifier = request.query_params.get('identifier', False) - state_value = request.query_params.get('cumulative_state', False) + identifier = request.query_params.get("identifier", False) + state_value = request.query_params.get("cumulative_state", False) if not identifier: - raise Http400('Query param \'identifier\' missing') + raise Http400("Query param 'identifier' missing") if not state_value: - raise Http400('Query param \'cumulative_state\' missing') + raise Http400("Query param 'cumulative_state' missing") try: cr = self.object.objects.get(identifier=identifier) @@ -103,73 +127,74 @@ def change_cumulative_state(self, request): raise Http404 if not cr.user_has_access(request): - raise Http403('You do not have permissions to modify this dataset') + raise Http403("You do not have permissions to modify this dataset") cr.request = request if cr.change_cumulative_state(state_value): # new version is created return_status = status.HTTP_200_OK - data = { 'new_version_created': self.get_serializer(cr).data['new_version_created'] } + data = {"new_version_created": self.get_serializer(cr).data["new_version_created"]} else: return_status = status.HTTP_204_NO_CONTENT data = None return Response(data=data, status=return_status) - @action(detail=False, methods=['post'], url_path="refresh_directory_content") + @action(detail=False, methods=["post"], url_path="refresh_directory_content") def refresh_directory_content(self, request): - cr_identifier = request.query_params.get('cr_identifier', False) - dir_identifier = request.query_params.get('dir_identifier', False) + cr_identifier = request.query_params.get("cr_identifier", False) + dir_identifier = request.query_params.get("dir_identifier", False) if not cr_identifier: - raise Http400('Query param \'cr_identifier\' missing.') + raise Http400("Query param 'cr_identifier' missing.") if not dir_identifier: - raise Http400('Query param \'dir_identifier\' missing.') + raise Http400("Query param 'dir_identifier' missing.") try: cr = self.object.objects.get(identifier=cr_identifier) except self.object.DoesNotExist: - raise Http404(f'CatalogRecord \'{cr_identifier}\' could not be found') + raise Http404(f"CatalogRecord '{cr_identifier}' could not be found") if not cr.user_has_access(request): - raise Http403('You do not have permissions to modify this dataset') + raise Http403("You do not have permissions to modify this dataset") cr.request = request new_version, n_added_files = cr.refresh_directory_content(dir_identifier) - data = { 'number_of_files_added': n_added_files } + data = {"number_of_files_added": n_added_files} if new_version: - data['new_version_created'] = self.get_serializer(cr).data['new_version_created'] + data["new_version_created"] = self.get_serializer(cr).data["new_version_created"] return Response(data=data, status=status.HTTP_200_OK) - @action(detail=False, methods=['post'], url_path="fix_deprecated") + @action(detail=False, methods=["post"], url_path="fix_deprecated") def fix_deprecated(self, request): - if not request.query_params.get('identifier', False): - raise Http400('Query param \'identifier\' missing. Please specify ?identifier=') + if not request.query_params.get("identifier", False): + raise Http400( + "Query param 'identifier' missing. Please specify ?identifier=" + ) try: - cr = self.object.objects.get(identifier=request.query_params['identifier']) + cr = self.object.objects.get(identifier=request.query_params["identifier"]) except self.object.DoesNotExist: raise Http404 if not cr.deprecated: - raise Http400('Requested catalog record is not deprecated') + raise Http400("Requested catalog record is not deprecated") cr.fix_deprecated() - data = { 'new_version_created': self.get_serializer(cr).data['new_version_created'] } + data = {"new_version_created": self.get_serializer(cr).data["new_version_created"]} return Response(data=data, status=status.HTTP_200_OK) def _save_and_publish_dataset(self, cr, action): try: - DataciteService().get_validated_datacite_json( - convert_cr_to_datacite_cr_json(cr), True) + DataciteService().get_validated_datacite_json(convert_cr_to_datacite_cr_json(cr), True) except DataciteException as e: raise Http400(str(e)) - super(CatalogRecord, cr).save(update_fields=['preservation_identifier']) + super(CatalogRecord, cr).save(update_fields=["preservation_identifier"]) DataciteDOIUpdate(cr, cr.preservation_identifier, action)() diff --git a/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py b/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py index bddcb072..c0a8e915 100755 --- a/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py +++ b/src/metax_api/api/rpc/base/views/elasticsearch_rpc.py @@ -21,8 +21,7 @@ class ElasticsearchRPC(CommonRPC): - - @action(detail=False, methods=['get'], url_path="map_refdata") + @action(detail=False, methods=["get"], url_path="map_refdata") def map_refdata(self, request): if not isinstance(django_settings, dict): @@ -30,69 +29,69 @@ def map_refdata(self, request): connection_params = RDL.get_connection_parameters(settings) # returns scan object as well but that is not needed here - esclient = RDL.get_es_imports(settings['HOSTS'], connection_params)[0] + esclient = RDL.get_es_imports(settings["HOSTS"], connection_params)[0] if not self.request.query_params: return Response(status=status.HTTP_200_OK) - elif '_mapping' in self.request.query_params: + elif "_mapping" in self.request.query_params: try: res = esclient.indices.get_mapping() except Exception as e: - raise Http400(f'Error when accessing elasticsearch. {e}') + raise Http400(f"Error when accessing elasticsearch. {e}") return Response(data=res, status=status.HTTP_200_OK) params = {} for k, v in self.request.query_params.items(): # python dict.items() keeps order so the url is always the first one - if '_data/' in k: - splitted = k.split('/') + if "_data/" in k: + splitted = k.split("/") if len(splitted) < 3: - _logger.info('Bad url to elasticsearch proxy') + _logger.info("Bad url to elasticsearch proxy") return Response(data=None, status=status.HTTP_204_NO_CONTENT) idx = splitted[0] type = splitted[1] - if '?' in splitted[2]: - action, first_param = splitted[2].split('?') + if "?" in splitted[2]: + action, first_param = splitted[2].split("?") else: action, first_param = splitted[2], None - if action != '_search': - _logger.info('Bad action to elasticsearch proxy') + if action != "_search": + _logger.info("Bad action to elasticsearch proxy") return Response(data=None, status=status.HTTP_204_NO_CONTENT) if first_param: params[first_param] = v - elif k == 'pretty': - params[k] = 'true' if v else 'false' + elif k == "pretty": + params[k] = "true" if v else "false" - elif k == 'q': + elif k == "q": try: # new ES client separates filters with a space - v = v.replace('+AND+', ' AND ') - v = v.replace('+OR+', ' OR ') - if 'type:' not in v: - params[k] = v + f' AND type:{type}' + v = v.replace("+AND+", " AND ") + v = v.replace("+OR+", " OR ") + if "type:" not in v: + params[k] = v + f" AND type:{type}" else: params[k] = v except: - _logger.info('Elasticsearch proxy has missing type. This should not happen') + _logger.info("Elasticsearch proxy has missing type. This should not happen") return Response(data=None, status=status.HTTP_204_NO_CONTENT) else: params[k] = v - if 'q' not in params: - params['q'] = f'type:{type}' + if "q" not in params: + params["q"] = f"type:{type}" try: res = esclient.search(index=idx, params=params) except Exception as e: - raise Http400(f'Error when accessing elasticsearch. {e}') + raise Http400(f"Error when accessing elasticsearch. {e}") - return Response(data=res, status=status.HTTP_200_OK) \ No newline at end of file + return Response(data=res, status=status.HTTP_200_OK) diff --git a/src/metax_api/api/rpc/base/views/file_rpc.py b/src/metax_api/api/rpc/base/views/file_rpc.py index 65b887e2..eb8dd169 100755 --- a/src/metax_api/api/rpc/base/views/file_rpc.py +++ b/src/metax_api/api/rpc/base/views/file_rpc.py @@ -22,54 +22,55 @@ class FileRPC(CommonRPC): - - @action(detail=False, methods=['post'], url_path="delete_project") + @action(detail=False, methods=["post"], url_path="delete_project") def delete_project(self, request): """ Marks files deleted, deprecates related datasets and removes all directories. """ - if 'project_identifier' not in request.query_params: - raise Http400({ 'detail': ['required query parameter project_identifier missing'] }) + if "project_identifier" not in request.query_params: + raise Http400({"detail": ["required query parameter project_identifier missing"]}) - return FileService.delete_project(request.query_params['project_identifier']) + return FileService.delete_project(request.query_params["project_identifier"]) - @action(detail=False, methods=['post'], url_path="flush_project") - def flush_project(self, request): # pragma: no cover + @action(detail=False, methods=["post"], url_path="flush_project") + def flush_project(self, request): # pragma: no cover """ Permanently delete an entire project's files and directories. WARNING! Does not check file association with datasets! Not meant for active production use!! """ - if settings.ENV == 'production': - raise Http400({ 'detail': ['API currently allowed only in test environments'] }) + if settings.ENV == "production": + raise Http400({"detail": ["API currently allowed only in test environments"]}) - if 'project_identifier' not in request.query_params: - raise Http400({ 'detail': ['project_identifier is a required query parameter'] }) + if "project_identifier" not in request.query_params: + raise Http400({"detail": ["project_identifier is a required query parameter"]}) - project = request.query_params['project_identifier'] + project = request.query_params["project_identifier"] - sql_delete_cr_files = ''' + sql_delete_cr_files = """ delete from metax_api_catalogrecord_files where file_id in ( select id from metax_api_file where project_identifier = %s ) - ''' + """ - sql_delete_files = ''' + sql_delete_files = """ delete from metax_api_file where project_identifier = %s - ''' + """ - sql_delete_directories = ''' + sql_delete_directories = """ delete from metax_api_directory where project_identifier = %s - ''' + """ - _logger.info('Flushing project %s on the request of user: %s' % (project, request.user.username)) + _logger.info( + "Flushing project %s on the request of user: %s" % (project, request.user.username) + ) with connection.cursor() as cr: cr.execute(sql_delete_cr_files, [project]) cr.execute(sql_delete_files, [project]) cr.execute(sql_delete_directories, [project]) - _logger.info('Permanently deleted all files and directories from project %s' % project) + _logger.info("Permanently deleted all files and directories from project %s" % project) return Response(data=None, status=status.HTTP_204_NO_CONTENT) diff --git a/src/metax_api/api/rpc/base/views/statistic_rpc.py b/src/metax_api/api/rpc/base/views/statistic_rpc.py index e4176cfc..96c4713f 100755 --- a/src/metax_api/api/rpc/base/views/statistic_rpc.py +++ b/src/metax_api/api/rpc/base/views/statistic_rpc.py @@ -18,111 +18,133 @@ _logger = logging.getLogger(__name__) -date_re = re.compile( - r'^\d{4}-\d{2}$' -) +date_re = re.compile(r"^\d{4}-\d{2}$") -class StatisticRPC(CommonRPC): - @action(detail=False, methods=['get'], url_path='all_datasets_cumulative') +class StatisticRPC(CommonRPC): + @action(detail=False, methods=["get"], url_path="all_datasets_cumulative") def all_datasets_cumulative(self, request): - if not request.query_params.get('from_date', None) or not request.query_params.get('to_date', None): - raise Http400('from_date and to_date parameters are required') + if not request.query_params.get("from_date", None) or not request.query_params.get( + "to_date", None + ): + raise Http400("from_date and to_date parameters are required") str_params = { - 'from_date': request.query_params.get('from_date', None), - 'to_date': request.query_params.get('to_date', None), + "from_date": request.query_params.get("from_date", None), + "to_date": request.query_params.get("to_date", None), } - if not date_re.match(str_params['from_date']) or not date_re.match(str_params['to_date']): - raise Http400('date parameter format is \'YYYY-MM\'') + if not date_re.match(str_params["from_date"]) or not date_re.match(str_params["to_date"]): + raise Http400("date parameter format is 'YYYY-MM'") - params = { param: request.query_params.get(param, None) for param in str_params } + params = {param: request.query_params.get(param, None) for param in str_params} - for boolean_param in ['latest', 'legacy', 'removed']: + for boolean_param in ["latest", "legacy", "removed"]: if boolean_param in request.query_params: params[boolean_param] = CS.get_boolean_query_param(request, boolean_param) return Response(StatisticService.total_datasets(**params)) - @action(detail=False, methods=['get'], url_path='catalog_datasets_cumulative') + @action(detail=False, methods=["get"], url_path="catalog_datasets_cumulative") def catalog_datasets_cumulative(self, request): - if not request.query_params.get('from_date', None) or not request.query_params.get('to_date', None): - raise Http400('from_date and to_date parameters are required') + if not request.query_params.get("from_date", None) or not request.query_params.get( + "to_date", None + ): + raise Http400("from_date and to_date parameters are required") params = { - 'from_date': request.query_params.get('from_date', None), - 'to_date': request.query_params.get('to_date', None), - 'data_catalog': request.query_params.get('data_catalog', None), + "from_date": request.query_params.get("from_date", None), + "to_date": request.query_params.get("to_date", None), + "data_catalog": request.query_params.get("data_catalog", None), } return Response(StatisticService.total_data_catalog_datasets(**params)) - @action(detail=False, methods=['get'], url_path='count_datasets') + @action(detail=False, methods=["get"], url_path="count_datasets") def count_datasets(self, request): str_params = [ - 'access_type', - 'data_catalog', - 'from_date', - 'metadata_owner_org', - 'metadata_provider_org', - 'metadata_provider_user', - 'preservation_state', - 'to_date' + "access_type", + "data_catalog", + "from_date", + "metadata_owner_org", + "metadata_provider_org", + "metadata_provider_user", + "preservation_state", + "to_date", ] - params = { param: request.query_params.get(param, None) for param in str_params } + params = {param: request.query_params.get(param, None) for param in str_params} - for boolean_param in ['deprecated', 'harvested', 'latest', 'legacy', 'removed']: + for boolean_param in ["deprecated", "harvested", "latest", "legacy", "removed"]: if boolean_param in request.query_params: params[boolean_param] = CS.get_boolean_query_param(request, boolean_param) return Response(StatisticService.count_datasets(**params)) - @action(detail=False, methods=['get'], url_path='deprecated_datasets_cumulative') + @action(detail=False, methods=["get"], url_path="deprecated_datasets_cumulative") def deprecated_datasets_cumulative(self, request): - if not request.query_params.get('from_date', None) or not request.query_params.get('to_date', None): - raise Http400('from_date and to_date parameters are required') + if not request.query_params.get("from_date", None) or not request.query_params.get( + "to_date", None + ): + raise Http400("from_date and to_date parameters are required") params = { - 'from_date': request.query_params.get('from_date', None), - 'to_date': request.query_params.get('to_date', None), + "from_date": request.query_params.get("from_date", None), + "to_date": request.query_params.get("to_date", None), } return Response(StatisticService.deprecated_datasets_cumulative(**params)) - @action(detail=False, methods=['get'], url_path='end_user_datasets_cumulative') + @action(detail=False, methods=["get"], url_path="end_user_datasets_cumulative") def end_user_datasets_cumulative(self, request): - if not request.query_params.get('from_date', None) or not request.query_params.get('to_date', None): - raise Http400('from_date and to_date parameters are required') + if not request.query_params.get("from_date", None) or not request.query_params.get( + "to_date", None + ): + raise Http400("from_date and to_date parameters are required") params = { - 'from_date': request.query_params.get('from_date', None), - 'to_date': request.query_params.get('to_date', None), + "from_date": request.query_params.get("from_date", None), + "to_date": request.query_params.get("to_date", None), } return Response(StatisticService.total_end_user_datasets(**params)) - @action(detail=False, methods=['get'], url_path='harvested_datasets_cumulative') + @action(detail=False, methods=["get"], url_path="harvested_datasets_cumulative") def harvested_datasets_cumulative(self, request): - if not request.query_params.get('from_date', None) or not request.query_params.get('to_date', None): - raise Http400('from_date and to_date parameters are required') + if not request.query_params.get("from_date", None) or not request.query_params.get( + "to_date", None + ): + raise Http400("from_date and to_date parameters are required") params = { - 'from_date': request.query_params.get('from_date', None), - 'to_date': request.query_params.get('to_date', None), + "from_date": request.query_params.get("from_date", None), + "to_date": request.query_params.get("to_date", None), } return Response(StatisticService.total_harvested_datasets(**params)) - @action(detail=False, methods=['get'], url_path='organization_datasets_cumulative') + @action(detail=False, methods=["get"], url_path="organization_datasets_cumulative") def organization_datasets_cumulative(self, request): - if not request.query_params.get('from_date', None) or not request.query_params.get('to_date', None): - raise Http400('from_date and to_date parameters are required') + if not request.query_params.get("from_date", None) or not request.query_params.get( + "to_date", None + ): + raise Http400("from_date and to_date parameters are required") params = { - 'from_date': request.query_params.get('from_date', None), - 'to_date': request.query_params.get('to_date', None), - 'metadata_owner_org': request.query_params.get('metadata_owner_org', None), + "from_date": request.query_params.get("from_date", None), + "to_date": request.query_params.get("to_date", None), + "metadata_owner_org": request.query_params.get("metadata_owner_org", None), } return Response(StatisticService.total_organization_datasets(**params)) - @action(detail=False, methods=['get'], url_path='unused_files') + @action(detail=False, methods=["get"], url_path="unused_files") def unused_files(self, request): return Response(StatisticService.unused_files()) + + @action(detail=False, methods=["get"], url_path="count_files") + def count_files(self, request): + if not request.query_params.get("projects"): + raise Http400("projects parameter is required") + + params = { + "projects": list(CS.get_list_query_param(request, "projects")), + "removed": request.query_params.get("removed", None), + } + + return Response(StatisticService.count_files(**params)) diff --git a/src/metax_api/api/rpc/v2/router.py b/src/metax_api/api/rpc/v2/router.py index 792edd95..859f1eb1 100755 --- a/src/metax_api/api/rpc/v2/router.py +++ b/src/metax_api/api/rpc/v2/router.py @@ -29,18 +29,17 @@ class CustomRouter(DefaultRouter): - def get_default_basename(self, viewset): """ When a viewset has no queryset set, or base_name is not passed to a router as the 3rd parameter, automatically determine base name. """ - return viewset.__class__.__name__.split('RPC')[0] + return viewset.__class__.__name__.split("RPC")[0] router = CustomRouter(trailing_slash=False) -router.register(r'datasets/?', DatasetRPC) -router.register(r'files/?', FileRPC) -router.register(r'statistics/?', StatisticRPC) +router.register(r"datasets/?", DatasetRPC) +router.register(r"files/?", FileRPC) +router.register(r"statistics/?", StatisticRPC) api_urlpatterns = router.urls diff --git a/src/metax_api/api/rpc/v2/views/dataset_rpc.py b/src/metax_api/api/rpc/v2/views/dataset_rpc.py index 752b7447..4ca86b70 100755 --- a/src/metax_api/api/rpc/v2/views/dataset_rpc.py +++ b/src/metax_api/api/rpc/v2/views/dataset_rpc.py @@ -7,6 +7,7 @@ import logging +from django.db import DatabaseError, transaction from django.http import Http404 from rest_framework import status from rest_framework.decorators import action @@ -30,15 +31,17 @@ def get_object(self): RPC api does not handle parameters the exact same way as REST api, so need to re-define get_object() to work properly. """ - if not self.request.query_params.get('identifier', False): - raise Http400('Query param \'identifier\' missing. Please specify ?identifier=') + if not self.request.query_params.get("identifier", False): + raise Http400( + "Query param 'identifier' missing. Please specify ?identifier=" + ) params = {} try: - params['pk'] = int(self.request.query_params['identifier']) + params["pk"] = int(self.request.query_params["identifier"]) except ValueError: - params['identifier'] = self.request.query_params['identifier'] + params["identifier"] = self.request.query_params["identifier"] try: cr = self.object.objects.get(**params) @@ -51,13 +54,13 @@ def get_object(self): return cr - @action(detail=False, methods=['post'], url_path="change_cumulative_state") + @action(detail=False, methods=["post"], url_path="change_cumulative_state") def change_cumulative_state(self, request): - state_value = request.query_params.get('cumulative_state', False) + state_value = request.query_params.get("cumulative_state", False) if not state_value: - raise Http400('Query param \'cumulative_state\' missing') + raise Http400("Query param 'cumulative_state' missing") cr = self.get_object() @@ -67,49 +70,55 @@ def change_cumulative_state(self, request): return Response(status=status.HTTP_204_NO_CONTENT) - @action(detail=False, methods=['post'], url_path="create_draft") + @action(detail=False, methods=["post"], url_path="create_draft") def create_draft(self, request): + try: + with transaction.atomic(): + cr = self.get_object() - cr = self.get_object() - - cr.create_draft() + cr.create_draft() - return Response( - data={ - 'id': cr.next_draft.id, - 'identifier': cr.next_draft.identifier - }, - status=status.HTTP_201_CREATED - ) + return Response( + data={"id": cr.next_draft.id, "identifier": cr.next_draft.identifier}, + status=status.HTTP_201_CREATED, + ) + except DatabaseError: + return Response({'error': 'Failed to create draft'}, status=status.HTTP_400_BAD_REQUEST) - @action(detail=False, methods=['post'], url_path="create_new_version") + @action(detail=False, methods=["post"], url_path="create_new_version") def create_new_version(self, request): - - cr = self.get_object() - - cr.create_new_version() - - return Response( - data={ - 'id': cr.next_dataset_version.id, - 'identifier': cr.next_dataset_version.identifier - }, - status=status.HTTP_201_CREATED - ) - - @action(detail=False, methods=['post'], url_path="publish_dataset") + try: + with transaction.atomic(): + cr = self.get_object() + + cr.create_new_version() + + return Response( + data={ + "id": cr.next_dataset_version.id, + "identifier": cr.next_dataset_version.identifier, + }, + status=status.HTTP_201_CREATED, + ) + except DatabaseError: + return Response({'error': 'Failed to create a new version'}, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=["post"], url_path="publish_dataset") def publish_dataset(self, request): + try: + with transaction.atomic(): + cr = self.get_object() - cr = self.get_object() - - cr.publish_dataset() + cr.publish_dataset() - return Response( - data={ 'preferred_identifier': cr.preferred_identifier }, - status=status.HTTP_200_OK - ) + return Response( + data={"preferred_identifier": cr.preferred_identifier}, + status=status.HTTP_200_OK, + ) + except DatabaseError: + return Response({'error': 'Failed to publish dataset'}, status=status.HTTP_400_BAD_REQUEST) - @action(detail=False, methods=['post'], url_path="merge_draft") + @action(detail=False, methods=["post"], url_path="merge_draft") def merge_draft(self, request): cr = self.get_object() @@ -118,17 +127,14 @@ def merge_draft(self, request): # the identifier points to the original published dataset. identifier_of_published = cr.merge_draft() - return Response( - data={ 'identifier': identifier_of_published }, - status=status.HTTP_200_OK - ) + return Response(data={"identifier": identifier_of_published}, status=status.HTTP_200_OK) # ensure some v1 api endpoints cant be called in v2 api - @action(detail=False, methods=['post'], url_path="refresh_directory_content") + @action(detail=False, methods=["post"], url_path="refresh_directory_content") def refresh_directory_content(self, request): raise Http501() - @action(detail=False, methods=['post'], url_path="fix_deprecated") + @action(detail=False, methods=["post"], url_path="fix_deprecated") def fix_deprecated(self, request): raise Http501() diff --git a/src/metax_api/checks.py b/src/metax_api/checks.py index 24e94bb4..0cf9468c 100755 --- a/src/metax_api/checks.py +++ b/src/metax_api/checks.py @@ -3,7 +3,9 @@ from watchman.decorators import check from metax_api.services.redis_cache_service import RedisClient -from metax_api.tasks.refdata.refdata_indexer.service.elasticsearch_service import ElasticSearchService +from metax_api.tasks.refdata.refdata_indexer.service.elasticsearch_service import ( + ElasticSearchService, +) logger = logging.getLogger(__name__) @@ -40,6 +42,4 @@ def redis_check(): return {"redis": [{"key: reference_data": {"ok": True}}]} except Exception as e: logger.error(e) - return { - "redis": {"ok": False, "error": str(e), "traceback": str(e.__traceback__)} - } + return {"redis": {"ok": False, "error": str(e), "traceback": str(e.__traceback__)}} diff --git a/src/metax_api/exceptions/http_exceptions.py b/src/metax_api/exceptions/http_exceptions.py index 050848af..443601cc 100755 --- a/src/metax_api/exceptions/http_exceptions.py +++ b/src/metax_api/exceptions/http_exceptions.py @@ -24,43 +24,50 @@ def __init__(self, detail=None, code=None): } """ -class MetaxAPIException(APIException): +class MetaxAPIException(APIException): def __init__(self, detail=None, code=None): """ Override so that exceptions can be used in a more simple manner such that the message always ends up in an array. """ if type(detail) is str: - detail = { 'detail': [detail] } + detail = {"detail": [detail]} super().__init__(detail=detail, code=code) + class Http400(MetaxAPIException): # bad request status_code = 400 + class Http401(MetaxAPIException): # unauthorized # note: request is missing authentication information, or it was wrong status_code = 401 + class Http403(MetaxAPIException): # forbidden # note: request user is correctly authenticated, but has no permission status_code = 403 + class Http412(MetaxAPIException): # precondition failed status_code = 412 + class Http500(MetaxAPIException): # internal server error status_code = 500 + class Http501(MetaxAPIException): # not implemented status_code = 501 + class Http503(MetaxAPIException): # service unavailable status_code = 503 diff --git a/src/metax_api/management/commands/first_time_setup.py b/src/metax_api/management/commands/first_time_setup.py index 6a28f634..de90affd 100644 --- a/src/metax_api/management/commands/first_time_setup.py +++ b/src/metax_api/management/commands/first_time_setup.py @@ -13,4 +13,4 @@ def handle(self, *args, **options): call_command("reload_refdata_cache") call_command("loaddata", "metax_api/tests/testdata/test_data.json") call_command("loadinitialdata") - logger.info("All first time setup commands completed successfully") \ No newline at end of file + logger.info("All first time setup commands completed successfully") diff --git a/src/metax_api/management/commands/fix_file_counts.py b/src/metax_api/management/commands/fix_file_counts.py index 8bdc7a53..1c59805e 100644 --- a/src/metax_api/management/commands/fix_file_counts.py +++ b/src/metax_api/management/commands/fix_file_counts.py @@ -6,12 +6,13 @@ logger = logging.getLogger(__name__) + class Command(BaseCommand): def handle(self, *args, **options): dirs_with_no_files = Directory.objects_unfiltered.all() dir_sum = dirs_with_no_files.count() logger.info(f"fix_file_counts command found {dir_sum} directories") - i=0 + i = 0 for dir in dirs_with_no_files: i += 1 try: @@ -19,4 +20,4 @@ def handle(self, *args, **options): except Exception as e: logger.error(f"can't fix filecount for directory {i}/{dir_sum}") logger.info(f"folder {i}/{dir_sum} has {dir.file_count} files after recalculation") - logger.info(f"fix_file_counts command executed successfully") \ No newline at end of file + logger.info(f"fix_file_counts command executed successfully") diff --git a/src/metax_api/management/commands/fix_file_counts_cr.py b/src/metax_api/management/commands/fix_file_counts_cr.py index 6768ffc7..d709378c 100644 --- a/src/metax_api/management/commands/fix_file_counts_cr.py +++ b/src/metax_api/management/commands/fix_file_counts_cr.py @@ -6,14 +6,17 @@ logger = logging.getLogger(__name__) + class Command(BaseCommand): def handle(self, *args, **options): CRS = CatalogRecord.objects.all() crs_sum = CRS.count() - logger.info(f"fix_file_counts command found {crs_sum} catalog records with file_count=0 and byte_size=0") + logger.info( + f"fix_file_counts command found {crs_sum} catalog records with file_count=0 and byte_size=0" + ) i = 1 for catalog_record in CRS: logger.info(f"Calculating {i}/{crs_sum} {catalog_record.identifier} ") catalog_record.calculate_directory_byte_sizes_and_file_counts() i += 1 - logger.info(f"fix_file_counts command executed successfully") \ No newline at end of file + logger.info(f"fix_file_counts command executed successfully") diff --git a/src/metax_api/management/commands/fix_file_counts_for_cr.py b/src/metax_api/management/commands/fix_file_counts_for_cr.py index 2409ae14..cba88c3b 100644 --- a/src/metax_api/management/commands/fix_file_counts_for_cr.py +++ b/src/metax_api/management/commands/fix_file_counts_for_cr.py @@ -6,10 +6,11 @@ logger = logging.getLogger(__name__) + class Command(BaseCommand): def handle(self, *args, **options): CRS = CatalogRecord.objects.all() for catalog_record in CRS: catalog_record.calculate_directory_byte_sizes_and_file_counts() logger.info(f"Calculating {catalog_record.identifier} ") - logger.info(f"fix_file_counts command executed successfully") \ No newline at end of file + logger.info(f"fix_file_counts command executed successfully") diff --git a/src/metax_api/management/commands/load_data_to_TTV.py b/src/metax_api/management/commands/load_data_to_TTV.py index a451fd9a..651d1ff6 100644 --- a/src/metax_api/management/commands/load_data_to_TTV.py +++ b/src/metax_api/management/commands/load_data_to_TTV.py @@ -20,29 +20,31 @@ class User: def __init__(self): self.is_service = True + class Request: def __init__(self, user): self.user = user self.query_params = [] - self.method = 'POST' + self.method = "POST" + class Command(BaseCommand): help = "Upload all existing data to TTV's RabbitMQ queue" def handle(self, *args, **options): - catalog_records = CatalogRecord.objects.filter(state='published') + catalog_records = CatalogRecord.objects.filter(state="published") aff_rows = 0 user = User() request = Request(user) - context = {'request': request} + context = {"request": request} for catalog_record in catalog_records: serializer = catalog_record.serializer_class cr_json = serializer(catalog_record, context=context).data - cr_json['data_catalog'] = {'catalog_json': catalog_record.data_catalog.catalog_json} + cr_json["data_catalog"] = {"catalog_json": catalog_record.data_catalog.catalog_json} - RabbitMQService.publish(cr_json, routing_key='create', exchange="TTV-datasets") + RabbitMQService.publish(cr_json, routing_key="create", exchange="TTV-datasets") aff_rows += 1 logger.info("All catalog records published to TTV exchange") diff --git a/src/metax_api/management/commands/loadinitialdata.py b/src/metax_api/management/commands/loadinitialdata.py index eb904c62..641022f2 100755 --- a/src/metax_api/management/commands/loadinitialdata.py +++ b/src/metax_api/management/commands/loadinitialdata.py @@ -16,6 +16,7 @@ logger = logging.getLogger(__name__) + class Command(BaseCommand): help = "Load initial data for Metax: Data catalogs, file storages." @@ -25,7 +26,10 @@ def handle(self, *args, **options): data_catalogs = json.load(f) for json_dc in data_catalogs: try: - dc = DataCatalog(catalog_json=json_dc["catalog_json"], date_created=datetime.datetime.now()) + dc = DataCatalog( + catalog_json=json_dc["catalog_json"], + date_created=datetime.datetime.now(), + ) dc.catalog_record_services_create = json_dc["catalog_record_services_create"] dc.catalog_record_services_edit = json_dc["catalog_record_services_edit"] dc.catalog_record_services_read = json_dc["catalog_record_services_read"] @@ -38,7 +42,10 @@ def handle(self, *args, **options): storages = json.load(f) for fs in storages: try: - fs = FileStorage(file_storage_json=fs["file_storage_json"], date_created=datetime.datetime.now()) + fs = FileStorage( + file_storage_json=fs["file_storage_json"], + date_created=datetime.datetime.now(), + ) fs.save() except IntegrityError as e: logger.error("filestorage already exists in the database") diff --git a/src/metax_api/management/commands/update_orgs.py b/src/metax_api/management/commands/update_orgs.py index 46515fb6..fb81d844 100644 --- a/src/metax_api/management/commands/update_orgs.py +++ b/src/metax_api/management/commands/update_orgs.py @@ -1,6 +1,5 @@ import csv import logging -from collections import OrderedDict from dataclasses import asdict, dataclass, field from typing import List @@ -23,31 +22,21 @@ ] -@dataclass() +@dataclass(order=True) class Organization: - org_name_fi: str = field() - org_name_en: str = field() - org_code: str = field() - org_name_sv: str = field(default="") - unit_sub_code: str = field(default="") - unit_name: str = field(default="") - org_isni: str = field(default="") - org_csc: str = field(default="") - unit_main_code: str = field(default="") - - def compare(self, other): - """Not overriding default __cmp__ for clarity""" - if ( - self.org_code == other.org_code - and self.unit_sub_code == other.unit_sub_code - and self.unit_main_code == other.unit_main_code - ): - return True - return False + org_name_fi: str = field(compare=False) + org_name_en: str = field(compare=False) + org_code: str = field(compare=True) + org_name_sv: str = field(default="", compare=False) + unit_sub_code: str = field(default="", compare=True) + unit_name: str = field(default="", compare=False) + org_isni: str = field(default="", compare=False) + org_csc: str = field(default="", compare=False) + unit_main_code: str = field(default="", compare=True) def compare_and_update(self, other): changes = 0 - match = self.compare(other) + match = self == other if match: if self.org_name_fi != other.org_name_fi: self.org_name_fi = other.org_name_fi @@ -61,15 +50,36 @@ def compare_and_update(self, other): if self.unit_name != other.unit_name: self.unit_name = other.unit_name changes += 1 + if self.org_isni is None and other.org_isni is not None: + self.org_isni = other.org_isni + changes += 1 + elif self.org_isni is not None and other.org_isni is None: + other.org_isni = self.org_isni + changes += 1 + + if self.org_csc is None and other.org_csc is not None: + self.org_csc = other.org_csc + changes += 1 + elif self.org_csc is not None and other.org_csc is None: + other.org_csc = self.org_csc + changes += 1 + if self.unit_name.endswith(";;"): + self.unit_name = self.unit_name[:-2] return match, changes def __str__(self): return f"{self.org_code}-{self.unit_sub_code}-{self.unit_name}" + def __post_init__(self): + if self.unit_name.endswith(";;"): + self.unit_name = self.unit_name[:-2] + def get_orgs_from_api() -> List[Organization]: - res = requests.get("https://researchfi-api-production-researchfi.rahtiapp.fi/portalapi/organization/_search") + res = requests.get( + "https://researchfi-api-production-researchfi.rahtiapp.fi/portalapi/organization/_search?size=100" + ) data = res.json() orgs_json = data["hits"]["hits"] @@ -127,15 +137,18 @@ def handle(self, *args, **options): for a in api_orgs: match, changes = i.compare_and_update(a) if match and changes > 0: - logger.info(f"updated org {i.org_code} with {changes} changes") + logger.info( + f"updated org {i.org_name_fi} with {changes} changes in unit: {a.unit_name}" + ) union.append(i) # add missing orgs to local ones added = 0 for i in api_orgs: match = False for a in loc_orgs: - match = i.compare(a) + match = i == a if not match: + # logger.info(f"adding missing org {i.org_name_fi}, {i.unit_name}") union.append(i) added += 1 logger.info(f"Added {added} organisations from research.fi to local org list") @@ -143,16 +156,27 @@ def handle(self, *args, **options): s = sorted(union, key=lambda i: (i.org_name_fi, i.unit_name)) with open(settings.ORG_FILE_PATH, "w") as f: logger.info("writing updated csv") - no_duplicates = OrderedDict() + no_duplicates = [] for c in s: - no_duplicates[str(c)] = c + if c not in no_duplicates: + no_duplicates.append(c) - csv_serialized = [asdict(v) for k, v in no_duplicates.items()] + csv_serialized = [asdict(v) for v in no_duplicates] writer = csv.DictWriter( f, fieldnames=CSV_HEADERS, ) writer.writeheader() for i in csv_serialized: + # Don't write header as row + if i["org_name_fi"] == "org_name_fi": + continue + # Malformed values from TTV api + if i["unit_name"] == "LÄÄKETIETEELLINEN TIEDEKUNTA": + continue + if "Ã…bo" in i["unit_name"]: + i["unit_name"] = str(i["unit_name"]).replace("Ã…bo", "Åbo") + if "ÖS" in i["unit_name"]: + i["unit_name"] = str(i["unit_name"]).replace("ÖS", "Ö") writer.writerow(i) logger.info("successfully updated organization csv") diff --git a/src/metax_api/middleware/add_last_modified_header_to_response.py b/src/metax_api/middleware/add_last_modified_header_to_response.py index 883cf498..d2461fa0 100755 --- a/src/metax_api/middleware/add_last_modified_header_to_response.py +++ b/src/metax_api/middleware/add_last_modified_header_to_response.py @@ -63,9 +63,7 @@ def _add_last_modified_header_to_response(response): if modified: modified_dt = parse_timestamp_string_to_tz_aware_datetime(modified) if modified_dt: - date_modified_in_gmt = timezone.localtime( - modified_dt, timezone=tz("GMT") - ) + date_modified_in_gmt = timezone.localtime(modified_dt, timezone=tz("GMT")) response["Last-Modified"] = date_modified_in_gmt.strftime( "%a, %d %b %Y %H:%M:%S GMT" ) diff --git a/src/metax_api/middleware/identifyapicaller.py b/src/metax_api/middleware/identifyapicaller.py index 3859f233..85b04e36 100755 --- a/src/metax_api/middleware/identifyapicaller.py +++ b/src/metax_api/middleware/identifyapicaller.py @@ -117,8 +117,7 @@ def _identify_api_caller(self, request): { "detail": [ "Invalid HTTP authorization method. Ensure you included on of the following " - "methods inside the auth header: %s" - % ", ".join(self.ALLOWED_AUTH_METHODS) + "methods inside the auth header: %s" % ", ".join(self.ALLOWED_AUTH_METHODS) ] } ) @@ -139,9 +138,7 @@ def _identify_api_caller(self, request): elif auth_method.lower() == "bearer": self._auth_bearer(request, auth_b64) else: - raise Exception( - "The allowed auth method %s is missing handling" % auth_method - ) + raise Exception("The allowed auth method %s is missing handling" % auth_method) return request @@ -194,9 +191,7 @@ def _auth_bearer(self, request, auth_b64): if len(token.get("CSCUserName", "")) > 0: request.user.username = token["CSCUserName"] else: - _logger.warning( - "id_token does not contain valid user id: fairdataid or cscusername" - ) + _logger.warning("id_token does not contain valid user id: fairdataid or cscusername") raise Http403 request.user.is_service = False diff --git a/src/metax_api/middleware/request_logging.py b/src/metax_api/middleware/request_logging.py index 827b7057..0f90ed52 100755 --- a/src/metax_api/middleware/request_logging.py +++ b/src/metax_api/middleware/request_logging.py @@ -49,29 +49,21 @@ def get_username(self, request): if "Basic" in auth_header: try: user_type = "service" - user = ( - b64decode(auth_header.split(" ")[1]).decode("utf-8").split(":")[0] - ) + user = b64decode(auth_header.split(" ")[1]).decode("utf-8").split(":")[0] except: _logger.exception("Could not extract username from http auth header") elif "Bearer" in auth_header or "bearer" in auth_header: try: user_type = "end_user" user = json_loads( - b64decode(auth_header.split(" ")[1].split(".")[1] + "===").decode( - "utf-8" - ) + b64decode(auth_header.split(" ")[1].split(".")[1] + "===").decode("utf-8") )["CSCUserName"] except: # dont log as an error or crash, since we dont want to get bothered by # errors about malformed tokens. auth middleware is going to reject this # token later too. - _logger.info( - "Faulty token: Could not extract username from bearer token" - ) + _logger.info("Faulty token: Could not extract username from bearer token") else: - _logger.info( - "HTTP Auth method not basic or bearer - unable to get username" - ) + _logger.info("HTTP Auth method not basic or bearer - unable to get username") return user, user_type diff --git a/src/metax_api/migrations/0033_auto_20210201_1551.py b/src/metax_api/migrations/0033_auto_20210201_1551.py index e90ea617..14f47595 100755 --- a/src/metax_api/migrations/0033_auto_20210201_1551.py +++ b/src/metax_api/migrations/0033_auto_20210201_1551.py @@ -11,7 +11,7 @@ def recount_directory_files(apps, schema_editor): logger.info(f"found {dirs_with_no_files.count()} directories without files") aff_rows = 0 for dir in dirs_with_no_files: - dir.calculate_byte_size_and_file_count() + # dir.calculate_byte_size_and_file_count() aff_rows += 1 logger.info(f"migration 0033 complete with {aff_rows} affected rows") diff --git a/src/metax_api/migrations/0034_apierror.py b/src/metax_api/migrations/0034_apierror.py new file mode 100644 index 00000000..7329870a --- /dev/null +++ b/src/metax_api/migrations/0034_apierror.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.8 on 2021-06-04 07:03 + +from datetime import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0033_auto_20210201_1551'), + ] + + operations = [ + migrations.CreateModel( + name='ApiError', + fields=[ + ('id', models.BigAutoField(editable=False, primary_key=True, serialize=False)), + ('identifier', models.CharField(max_length=200, unique=True)), + ('error', models.JSONField()), + ('date_created', models.DateTimeField(default=datetime.now)), + ], + ), + ] diff --git a/src/metax_api/migrations/0035_auto_20210804_1607.py b/src/metax_api/migrations/0035_auto_20210804_1607.py new file mode 100644 index 00000000..56b89cce --- /dev/null +++ b/src/metax_api/migrations/0035_auto_20210804_1607.py @@ -0,0 +1,29 @@ +# Generated by Django 3.1.12 on 2021-08-04 13:07 + +from django.db import migrations, models +import metax_api.utils.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0034_apierror'), + ] + + operations = [ + migrations.CreateModel( + name='DeletedObject', + fields=[ + ('id', models.BigAutoField(editable=False, primary_key=True, serialize=False)), + ('identifier', models.CharField(max_length=200, unique=True)), + ('model_name', models.CharField(max_length=200)), + ('object_data', models.JSONField()), + ('date_deleted', models.DateTimeField(default=metax_api.utils.utils.get_tz_aware_now_without_micros)), + ], + ), + migrations.AlterField( + model_name='apierror', + name='date_created', + field=models.DateTimeField(default=metax_api.utils.utils.get_tz_aware_now_without_micros), + ), + ] diff --git a/src/metax_api/migrations/0036_remove_deletedobject_identifier.py b/src/metax_api/migrations/0036_remove_deletedobject_identifier.py new file mode 100644 index 00000000..136bef0f --- /dev/null +++ b/src/metax_api/migrations/0036_remove_deletedobject_identifier.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.12 on 2021-08-06 10:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0035_auto_20210804_1607'), + ] + + operations = [ + migrations.RemoveField( + model_name='deletedobject', + name='identifier', + ), + ] diff --git a/src/metax_api/migrations/0037_auto_20210811_1037.py b/src/metax_api/migrations/0037_auto_20210811_1037.py new file mode 100644 index 00000000..b7798fc2 --- /dev/null +++ b/src/metax_api/migrations/0037_auto_20210811_1037.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.12 on 2021-08-11 07:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0036_remove_deletedobject_identifier'), + ] + + operations = [ + migrations.AlterField( + model_name='deletedobject', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/src/metax_api/migrations/0038_remove_catalogrecord_editor.py b/src/metax_api/migrations/0038_remove_catalogrecord_editor.py new file mode 100644 index 00000000..12f78871 --- /dev/null +++ b/src/metax_api/migrations/0038_remove_catalogrecord_editor.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.12 on 2021-08-30 08:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0037_auto_20210811_1037'), + ] + + operations = [ + migrations.RemoveField( + model_name='catalogrecord', + name='editor', + ), + ] diff --git a/src/metax_api/migrations/0039_auto_20210908_1151.py b/src/metax_api/migrations/0039_auto_20210908_1151.py new file mode 100644 index 00000000..dea97485 --- /dev/null +++ b/src/metax_api/migrations/0039_auto_20210908_1151.py @@ -0,0 +1,82 @@ +# Generated by Django 3.1.13 on 2021-09-08 08:51 +import json + +from django.db import migrations + +import logging + +logger = logging.getLogger(__name__) + +def build_email_string(f_name, l_name, provider, domain_type): + return f"{f_name}.{l_name}@{provider}.{domain_type}" + +def replace_access_granter(cr, old_name, old_email, old_user_id, new_name, new_email, new_user_id): + logger.info(f"replacing access granter: {old_name}, {old_email}, {old_user_id} with {new_name}, {new_email}, {new_user_id}") + if cr.access_granter: + if cr.access_granter["name"] == old_name: + cr.access_granter["name"] = new_name + logger.info("access granter name changed") + if cr.access_granter["email"] == old_email: + cr.access_granter["email"] = new_email + logger.info("access granter email changed") + if cr.access_granter["userid"] == old_user_id: + cr.access_granter["userid"] = new_user_id + logger.info("access granter user_id changed") + +def replace_metadata_provider_user(cr, old_user, new_user): + logger.info(f"replacing metadata_provider_user: {old_user} with new_user: {new_user}") + if cr.metadata_provider_user: + if cr.metadata_provider_user == old_user: + cr.metadata_provider_user = new_user + logger.info("metadata_provider_user changed") + +datasets = [ + "1acea3cc-c245-4f6d-80ee-f9c6a6c907be", + "72dc2050-70bc-4f46-9fc7-b3ac2bd58bdc", + "9341625d-ebbb-4625-a2db-6c0424cdcf85", + "4a66600c-79b2-4dcf-a497-69540b4d0817", + "51c7c6c4-0129-4856-be71-072582882cb5", + "56ab6296-9bc9-41b3-95ea-b4655aa7978b", + "66cc0770-08f9-4df3-ac6f-b65a15f5e73e", + "cr955e904-e3dd-4d7e-99f1-3fed446f9630" # test +] + +def change_metadata_provider_user(apps, schema_editor): + CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') + for cr in CatalogRecord.objects.filter(identifier__in=datasets): + try: + logger.info(f"changing access granter for cr {cr.identifier}") + old_mail = build_email_string("alma", "tuokko", "helsinki", "fi") + new_email = build_email_string("karina", "lukin", "helsinki", "fi") + replace_metadata_provider_user(cr, "tuokalma", "ksuomine") + replace_access_granter(cr, "Alma Tuokko", old_mail, "tuokalma", "Karina Lukin", new_email, "ksuomine") + cr.save() + logger.info("cr save successful") + except Exception as e: + logger.error(e) + + +def revert(apps, schema_editor): + CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') + for cr in CatalogRecord.objects.filter(identifier__in=datasets): + try: + logger.info(f"changing access granter for cr {cr.identifier}") + old_mail = build_email_string("alma", "tuokko", "helsinki", "fi") + new_email = build_email_string("karina", "lukin", "helsinki", "fi") + replace_metadata_provider_user(cr, "ksuomine", "tuokalma") + replace_access_granter(cr, "Karina Lukin", new_email, "ksuomine", "Alma Tuokko", old_mail, "tuokalma", ) + + cr.save() + logger.info("cr save successful") + except Exception as e: + logger.error(e) + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0038_remove_catalogrecord_editor'), + ] + + operations = [ + migrations.RunPython(change_metadata_provider_user, revert), + ] \ No newline at end of file diff --git a/src/metax_api/models/__init__.py b/src/metax_api/models/__init__.py index 2cd13040..ec08fb44 100755 --- a/src/metax_api/models/__init__.py +++ b/src/metax_api/models/__init__.py @@ -5,11 +5,13 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +from .api_error import ApiError from .catalog_record import AlternateRecordSet, CatalogRecord from .catalog_record_v2 import CatalogRecordV2 from .common import Common from .contract import Contract from .data_catalog import DataCatalog +from .deleted_object import DeletedObject from .directory import Directory from .file import File from .file_storage import FileStorage diff --git a/src/metax_api/models/api_error.py b/src/metax_api/models/api_error.py new file mode 100644 index 00000000..a465a6e5 --- /dev/null +++ b/src/metax_api/models/api_error.py @@ -0,0 +1,22 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT + +import logging + +from django.db import models +from django.db.models import JSONField + +from metax_api.utils import get_tz_aware_now_without_micros + +_logger = logging.getLogger(__name__) + +class ApiError(models.Model): + + id = models.BigAutoField(primary_key=True, editable=False) + identifier = models.CharField(max_length=200, unique=True, null=False) + error = JSONField(null=False) + date_created = models.DateTimeField(default=get_tz_aware_now_without_micros) \ No newline at end of file diff --git a/src/metax_api/models/catalog_record.py b/src/metax_api/models/catalog_record.py index a9c27fa4..9a499bde 100755 --- a/src/metax_api/models/catalog_record.py +++ b/src/metax_api/models/catalog_record.py @@ -37,17 +37,17 @@ from .directory import Directory from .file import File -READ_METHODS = ('GET', 'HEAD', 'OPTIONS') +READ_METHODS = ("GET", "HEAD", "OPTIONS") DEBUG = settings.DEBUG _logger = logging.getLogger(__name__) ACCESS_TYPES = { - 'open': 'http://uri.suomi.fi/codelist/fairdata/access_type/code/open', - 'login': 'http://uri.suomi.fi/codelist/fairdata/access_type/code/login', - 'permit': 'http://uri.suomi.fi/codelist/fairdata/access_type/code/permit', - 'embargo': 'http://uri.suomi.fi/codelist/fairdata/access_type/code/embargo', - 'restricted': 'http://uri.suomi.fi/codelist/fairdata/access_type/code/restricted' + "open": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "login": "http://uri.suomi.fi/codelist/fairdata/access_type/code/login", + "permit": "http://uri.suomi.fi/codelist/fairdata/access_type/code/permit", + "embargo": "http://uri.suomi.fi/codelist/fairdata/access_type/code/embargo", + "restricted": "http://uri.suomi.fi/codelist/fairdata/access_type/code/restricted", } @@ -57,6 +57,7 @@ PAS_CATALOG = settings.PAS_DATA_CATALOG_IDENTIFIER DFT_CATALOG = settings.DFT_DATA_CATALOG_IDENTIFIER + class DiscardRecord(Exception): pass @@ -73,7 +74,7 @@ class AlternateRecordSet(models.Model): id = models.BigAutoField(primary_key=True, editable=False) - def print_records(self): # pragma: no cover + def print_records(self): # pragma: no cover for r in self.records.all(): print(r.__repr__()) @@ -86,6 +87,7 @@ class DatasetVersionSet(models.Model): Note! Does not inherit from model Common, so does not have timestmap fields, and a delete is an actual delete. """ + id = models.BigAutoField(primary_key=True, editable=False) def get_listing(self): @@ -93,27 +95,31 @@ def get_listing(self): Return a list of record preferred_identifiers that belong in the same dataset version chain. Latest first. """ - records = self.records(manager='objects_unfiltered') \ - .filter(state=CatalogRecord.STATE_PUBLISHED) \ - .order_by('-date_created') + records = ( + self.records(manager="objects_unfiltered") + .filter(state=CatalogRecord.STATE_PUBLISHED) + .order_by("-date_created") + ) versions = [ { - 'identifier': r.identifier, - 'preferred_identifier': r.preferred_identifier, - 'removed': r.removed, - 'date_created': r.date_created.astimezone().isoformat(), - 'date_removed': r.date_removed.astimezone().isoformat() if r.date_removed else None + "identifier": r.identifier, + "preferred_identifier": r.preferred_identifier, + "removed": r.removed, + "date_created": r.date_created.astimezone().isoformat(), + "date_removed": r.date_removed.astimezone().isoformat() if r.date_removed else None, } for r in records ] # dont show the date_removed field at all if the value is None (record has not been removed) - versions = [{key: value for (key, value) in i.items() if value is not None} for i in versions] + versions = [ + {key: value for (key, value) in i.items() if value is not None} for i in versions + ] return versions - def print_records(self): # pragma: no cover + def print_records(self): # pragma: no cover for r in self.records.all(): print(r.__repr__()) @@ -125,65 +131,80 @@ class ResearchDatasetVersion(models.Model): metadata_version_identifier = models.CharField(max_length=200, unique=True) preferred_identifier = models.CharField(max_length=200) research_dataset = JSONField() - catalog_record = models.ForeignKey('CatalogRecord', on_delete=models.DO_NOTHING, - related_name='research_dataset_versions') + catalog_record = models.ForeignKey( + "CatalogRecord", + on_delete=models.DO_NOTHING, + related_name="research_dataset_versions", + ) class Meta: indexes = [ - models.Index(fields=['metadata_version_identifier']), + models.Index(fields=["metadata_version_identifier"]), ] def __str__(self): return self.__repr__() def __repr__(self): - return '<%s: %d, cr: %d, metadata_version_identifier: %s, stored_to_pas: %s>' \ - % ( - 'ResearchDatasetVersion', - self.id, - self.catalog_record_id, - self.metadata_version_identifier, - str(self.stored_to_pas), - ) + return "<%s: %d, cr: %d, metadata_version_identifier: %s, stored_to_pas: %s>" % ( + "ResearchDatasetVersion", + self.id, + self.catalog_record_id, + self.metadata_version_identifier, + str(self.stored_to_pas), + ) class CatalogRecordManager(CommonManager): - def get(self, *args, **kwargs): - if kwargs.get('using_dict', None): + if kwargs.get("using_dict", None): # for a simple "just get me the instance that equals this dict i have" search. # preferred_identifier is not a valid search key, since it wouldnt necessarily # work during an update (if preferred_identifier is being updated). # this is useful if during a request the url does not contain the identifier (bulk update), # and in generic operations where the type of object being handled is not known (also bulk operations). - row = kwargs.pop('using_dict') - if row.get('id', None): - kwargs['id'] = row['id'] - elif row.get('identifier', None): - kwargs['identifier'] = row['identifier'] - elif row.get('research_dataset', None) and row['research_dataset'].get('metadata_version_identifier', None): + row = kwargs.pop("using_dict") + if row.get("id", None): + kwargs["id"] = row["id"] + elif row.get("identifier", None): + kwargs["identifier"] = row["identifier"] + elif row.get("research_dataset", None) and row["research_dataset"].get( + "metadata_version_identifier", None + ): # todo probably remove at some point - kwargs['research_dataset__contains'] = { - 'metadata_version_identifier': row['research_dataset']['metadata_version_identifier'] + kwargs["research_dataset__contains"] = { + "metadata_version_identifier": row["research_dataset"][ + "metadata_version_identifier" + ] } else: raise ValidationError( - 'this operation requires an identifying key to be present: id, or identifier') + "this operation requires an identifying key to be present: id, or identifier" + ) return super(CatalogRecordManager, self).get(*args, **kwargs) - def get_id(self, metadata_version_identifier=None): # pragma: no cover + def get_id(self, metadata_version_identifier=None): # pragma: no cover """ Takes metadata_version_identifier, and returns the plain pk of the record. Useful for debugging """ if not metadata_version_identifier: - raise ValidationError('metadata_version_identifier is a required keyword argument') - cr = super(CatalogRecordManager, self).filter( - **{ 'research_dataset__contains': {'metadata_version_identifier': metadata_version_identifier } } - ).values('id').first() + raise ValidationError("metadata_version_identifier is a required keyword argument") + cr = ( + super(CatalogRecordManager, self) + .filter( + **{ + "research_dataset__contains": { + "metadata_version_identifier": metadata_version_identifier + } + } + ) + .values("id") + .first() + ) if not cr: raise Http404 - return cr['id'] + return cr["id"] class CatalogRecord(Common): @@ -207,23 +228,35 @@ class CatalogRecord(Common): PRESERVATION_STATE_IN_DISSEMINATION = 140 PRESERVATION_STATE_CHOICES = ( - (PRESERVATION_STATE_INITIALIZED, 'Initialized'), - (PRESERVATION_STATE_PROPOSED, 'Proposed for digital preservation'), - (PRESERVATION_STATE_TECHNICAL_METADATA_GENERATED, 'Technical metadata generated'), - (PRESERVATION_STATE_TECHNICAL_METADATA_GENERATED_FAILED, 'Technical metadata generation failed'), - (PRESERVATION_STATE_INVALID_METADATA, 'Invalid metadata'), - (PRESERVATION_STATE_METADATA_VALIDATION_FAILED, 'Metadata validation failed'), - (PRESERVATION_STATE_VALIDATED_METADATA_UPDATED, 'Validated metadata updated'), - (PRESERVATION_STATE_VALIDATING_METADATA, 'Validating metadata'), - (PRESERVATION_STATE_VALID_METADATA, 'Valid metadata'), - (PRESERVATION_STATE_METADATA_CONFIRMED, 'Metadata confirmed'), - (PRESERVATION_STATE_ACCEPTED_TO_PAS, 'Accepted to digital preservation'), - (PRESERVATION_STATE_IN_PACKAGING_SERVICE, 'in packaging service'), - (PRESERVATION_STATE_PACKAGING_FAILED, 'Packaging failed'), - (PRESERVATION_STATE_SIP_IN_INGESTION, 'SIP sent to ingestion in digital preservation service'), - (PRESERVATION_STATE_IN_PAS, 'in digital preservation'), - (PRESERVATION_STATE_REJECTED_FROM_PAS, 'Rejected in digital preservation service'), - (PRESERVATION_STATE_IN_DISSEMINATION, 'in dissemination'), + (PRESERVATION_STATE_INITIALIZED, "Initialized"), + (PRESERVATION_STATE_PROPOSED, "Proposed for digital preservation"), + ( + PRESERVATION_STATE_TECHNICAL_METADATA_GENERATED, + "Technical metadata generated", + ), + ( + PRESERVATION_STATE_TECHNICAL_METADATA_GENERATED_FAILED, + "Technical metadata generation failed", + ), + (PRESERVATION_STATE_INVALID_METADATA, "Invalid metadata"), + (PRESERVATION_STATE_METADATA_VALIDATION_FAILED, "Metadata validation failed"), + (PRESERVATION_STATE_VALIDATED_METADATA_UPDATED, "Validated metadata updated"), + (PRESERVATION_STATE_VALIDATING_METADATA, "Validating metadata"), + (PRESERVATION_STATE_VALID_METADATA, "Valid metadata"), + (PRESERVATION_STATE_METADATA_CONFIRMED, "Metadata confirmed"), + (PRESERVATION_STATE_ACCEPTED_TO_PAS, "Accepted to digital preservation"), + (PRESERVATION_STATE_IN_PACKAGING_SERVICE, "in packaging service"), + (PRESERVATION_STATE_PACKAGING_FAILED, "Packaging failed"), + ( + PRESERVATION_STATE_SIP_IN_INGESTION, + "SIP sent to ingestion in digital preservation service", + ), + (PRESERVATION_STATE_IN_PAS, "in digital preservation"), + ( + PRESERVATION_STATE_REJECTED_FROM_PAS, + "Rejected in digital preservation service", + ), + (PRESERVATION_STATE_IN_DISSEMINATION, "in dissemination"), ) CUMULATIVE_STATE_NO = 0 @@ -231,121 +264,186 @@ class CatalogRecord(Common): CUMULATIVE_STATE_CLOSED = 2 CUMULATIVE_STATE_CHOICES = ( - (CUMULATIVE_STATE_NO, 'no'), - (CUMULATIVE_STATE_YES, 'yes'), - (CUMULATIVE_STATE_CLOSED, 'closed') + (CUMULATIVE_STATE_NO, "no"), + (CUMULATIVE_STATE_YES, "yes"), + (CUMULATIVE_STATE_CLOSED, "closed"), ) - STATE_PUBLISHED = 'published' - STATE_DRAFT = 'draft' + STATE_PUBLISHED = "published" + STATE_DRAFT = "draft" - STATE_CHOICES = ( - (STATE_PUBLISHED, 'published'), - (STATE_DRAFT, 'draft') - ) + STATE_CHOICES = ((STATE_PUBLISHED, "published"), (STATE_DRAFT, "draft")) # MODEL FIELD DEFINITIONS # alternate_record_set = models.ForeignKey( - AlternateRecordSet, on_delete=models.SET_NULL, null=True, related_name='records', - help_text='Records which are duplicates of this record, but in another catalog.') + AlternateRecordSet, + on_delete=models.SET_NULL, + null=True, + related_name="records", + help_text="Records which are duplicates of this record, but in another catalog.", + ) - contract = models.ForeignKey(Contract, null=True, on_delete=models.DO_NOTHING, related_name='records') + contract = models.ForeignKey( + Contract, null=True, on_delete=models.DO_NOTHING, related_name="records" + ) - data_catalog = models.ForeignKey(DataCatalog, on_delete=models.DO_NOTHING, related_name='records') + data_catalog = models.ForeignKey( + DataCatalog, on_delete=models.DO_NOTHING, related_name="records" + ) state = models.CharField( choices=STATE_CHOICES, default=STATE_DRAFT, max_length=200, - help_text='Publishing state (published / draft) of the dataset.' + help_text="Publishing state (published / draft) of the dataset.", ) dataset_group_edit = models.CharField( - max_length=200, blank=True, null=True, - help_text='Group which is allowed to edit the dataset in this catalog record.') + max_length=200, + blank=True, + null=True, + help_text="Group which is allowed to edit the dataset in this catalog record.", + ) deprecated = models.BooleanField( - default=False, help_text='Is True when files attached to a dataset have been deleted in IDA.') + default=False, + help_text="Is True when files attached to a dataset have been deleted in IDA.", + ) use_doi_for_published = models.BooleanField( default=None, blank=True, null=True, - help_text='Is True when "Use_DOI" field is checked in Qvain Light for draft.') + help_text='Is True when "Use_DOI" field is checked in Qvain Light for draft.', + ) date_deprecated = models.DateTimeField(null=True) - _directory_data = JSONField(null=True, help_text='Stores directory data related to browsing files and directories') + _directory_data = JSONField( + null=True, + help_text="Stores directory data related to browsing files and directories", + ) - files = models.ManyToManyField(File, related_query_name='record') + files = models.ManyToManyField(File, related_query_name="record") identifier = models.CharField(max_length=200, unique=True, null=False) mets_object_identifier = ArrayField(models.CharField(max_length=200), null=True) - metadata_owner_org = models.CharField(max_length=200, null=True, - help_text='Actually non-nullable, but is derived from field metadata_provider_org if omitted.') - - metadata_provider_org = models.CharField(max_length=200, null=False, help_text='Non-modifiable after creation') + metadata_owner_org = models.CharField( + max_length=200, + null=True, + help_text="Actually non-nullable, but is derived from field metadata_provider_org if omitted.", + ) - metadata_provider_user = models.CharField(max_length=200, null=False, help_text='Non-modifiable after creation') + metadata_provider_org = models.CharField( + max_length=200, null=False, help_text="Non-modifiable after creation" + ) - editor = JSONField(null=True, help_text='Editor specific fields, such as owner_id, modified, record_identifier') + metadata_provider_user = models.CharField( + max_length=200, null=False, help_text="Non-modifiable after creation" + ) - preservation_dataset_version = models.OneToOneField('self', on_delete=models.DO_NOTHING, null=True, - related_name='preservation_dataset_origin_version', - help_text='Link between a PAS-stored dataset and the originating dataset.') + preservation_dataset_version = models.OneToOneField( + "self", + on_delete=models.DO_NOTHING, + null=True, + related_name="preservation_dataset_origin_version", + help_text="Link between a PAS-stored dataset and the originating dataset.", + ) preservation_description = models.CharField( - max_length=200, blank=True, null=True, help_text='Reason for accepting or rejecting PAS proposal.') + max_length=200, + blank=True, + null=True, + help_text="Reason for accepting or rejecting PAS proposal.", + ) preservation_reason_description = models.CharField( - max_length=200, blank=True, null=True, help_text='Reason for PAS proposal from the user.') + max_length=200, + blank=True, + null=True, + help_text="Reason for PAS proposal from the user.", + ) preservation_state = models.IntegerField( - choices=PRESERVATION_STATE_CHOICES, default=PRESERVATION_STATE_INITIALIZED, help_text='Record state in PAS.') + choices=PRESERVATION_STATE_CHOICES, + default=PRESERVATION_STATE_INITIALIZED, + help_text="Record state in PAS.", + ) - preservation_state_modified = models.DateTimeField(null=True, help_text='Date of last preservation state change.') + preservation_state_modified = models.DateTimeField( + null=True, help_text="Date of last preservation state change." + ) preservation_identifier = models.CharField(max_length=200, unique=True, null=True) research_dataset = JSONField() - next_draft = models.OneToOneField('self', on_delete=models.SET_NULL, null=True, - related_name='draft_of', - help_text='A draft of the next changes to be published on this dataset, in order to be able ' - 'to save progress, and continue later. Is created from a published dataset. ' - 'When the draft is published, changes are saved on top of the original dataset, ' - 'and the draft record is destroyed.') + next_draft = models.OneToOneField( + "self", + on_delete=models.SET_NULL, + null=True, + related_name="draft_of", + help_text="A draft of the next changes to be published on this dataset, in order to be able " + "to save progress, and continue later. Is created from a published dataset. " + "When the draft is published, changes are saved on top of the original dataset, " + "and the draft record is destroyed.", + ) - next_dataset_version = models.OneToOneField('self', on_delete=models.DO_NOTHING, null=True, - related_name='+') + next_dataset_version = models.OneToOneField( + "self", on_delete=models.DO_NOTHING, null=True, related_name="+" + ) - previous_dataset_version = models.OneToOneField('self', on_delete=models.DO_NOTHING, null=True, - related_name='+') + previous_dataset_version = models.OneToOneField( + "self", on_delete=models.DO_NOTHING, null=True, related_name="+" + ) dataset_version_set = models.ForeignKey( - DatasetVersionSet, on_delete=models.DO_NOTHING, null=True, related_name='records', - help_text='Records which are different dataset versions of each other.') + DatasetVersionSet, + on_delete=models.DO_NOTHING, + null=True, + related_name="records", + help_text="Records which are different dataset versions of each other.", + ) - cumulative_state = models.IntegerField(choices=CUMULATIVE_STATE_CHOICES, default=CUMULATIVE_STATE_NO) + cumulative_state = models.IntegerField( + choices=CUMULATIVE_STATE_CHOICES, default=CUMULATIVE_STATE_NO + ) - date_cumulation_started = models.DateTimeField(null=True, help_text='Date when cumulative_state was set to YES.') + date_cumulation_started = models.DateTimeField( + null=True, help_text="Date when cumulative_state was set to YES." + ) - date_cumulation_ended = models.DateTimeField(null=True, help_text='Date when cumulative_state was set to CLOSED.') + date_cumulation_ended = models.DateTimeField( + null=True, help_text="Date when cumulative_state was set to CLOSED." + ) - date_last_cumulative_addition = models.DateTimeField(null=True, default=None, - help_text='Date of last file addition while actively cumulative.') + date_last_cumulative_addition = models.DateTimeField( + null=True, + default=None, + help_text="Date of last file addition while actively cumulative.", + ) - access_granter = JSONField(null=True, default=None, - help_text='Stores data of REMS user who is currently granting access to this dataset') + access_granter = JSONField( + null=True, + default=None, + help_text="Stores data of REMS user who is currently granting access to this dataset", + ) - rems_identifier = models.CharField(max_length=200, null=True, default=None, - help_text='Defines corresponding catalog item in REMS service') + rems_identifier = models.CharField( + max_length=200, + null=True, + default=None, + help_text="Defines corresponding catalog item in REMS service", + ) - api_meta = JSONField(null=True, default=dict, - help_text='Saves api related info about the dataset. E.g. api version') + api_meta = JSONField( + null=True, + default=dict, + help_text="Saves api related info about the dataset. E.g. api version", + ) # END OF MODEL FIELD DEFINITIONS # @@ -380,39 +478,40 @@ class CatalogRecord(Common): class Meta: indexes = [ - models.Index(fields=['data_catalog']), - models.Index(fields=['identifier']), + models.Index(fields=["data_catalog"]), + models.Index(fields=["identifier"]), ] - ordering = ['id'] + ordering = ["id"] def __init__(self, *args, **kwargs): super(CatalogRecord, self).__init__(*args, **kwargs) self.track_fields( - 'api_meta', - 'access_granter', - 'cumulative_state', - 'data_catalog_id', - 'date_deprecated', - 'deprecated', - 'identifier', - 'metadata_owner_org', - 'metadata_provider_org', - 'metadata_provider_user', - 'preservation_state', - 'preservation_identifier', - 'research_dataset', - 'research_dataset.files', - 'research_dataset.directories', - 'research_dataset.total_files_byte_size', - 'research_dataset.total_remote_resources_byte_size', - 'research_dataset.metadata_version_identifier', - 'research_dataset.preferred_identifier', + "api_meta", + "access_granter", + "cumulative_state", + "data_catalog_id", + "date_deprecated", + "deprecated", + "identifier", + "metadata_owner_org", + "metadata_provider_org", + "metadata_provider_user", + "preservation_state", + "preservation_identifier", + "research_dataset", + "research_dataset.files", + "research_dataset.directories", + "research_dataset.total_files_byte_size", + "research_dataset.total_remote_resources_byte_size", + "research_dataset.metadata_version_identifier", + "research_dataset.preferred_identifier", ) from metax_api.api.rest.base.serializers import CatalogRecordSerializer + self.serializer_class = CatalogRecordSerializer self.api_version = 1 - def print_files(self): # pragma: no cover + def print_files(self): # pragma: no cover for f in self.files.all(): print(f) @@ -421,23 +520,29 @@ def user_has_access(self, request): In the future, will probably be more involved checking... """ if request.user.is_service: - if request.method == 'GET': + if request.method == "GET": return True - if not self._check_catalog_permissions(self.data_catalog.catalog_record_group_edit, - self.data_catalog.catalog_record_services_edit, request): + if not self._check_catalog_permissions( + self.data_catalog.catalog_record_group_edit, + self.data_catalog.catalog_record_services_edit, + request, + ): return False return True elif request.method in READ_METHODS: - if request.user.username is None: # unauthenticated user + if request.user.username == "": # unauthenticated user if self.state == self.STATE_PUBLISHED: return True else: raise Http404 - else: # enduser + else: # enduser if self.state == self.STATE_PUBLISHED: return True - elif self.state == self.STATE_DRAFT and self.metadata_provider_user == request.user.username: + elif ( + self.state == self.STATE_DRAFT + and self.metadata_provider_user == request.user.username + ): return True else: raise Http404 @@ -447,14 +552,14 @@ def user_has_access(self, request): def user_is_owner(self, request): if self.state == self.STATE_DRAFT and self.metadata_provider_user != request.user.username: - _logger.debug('404 due to state == draft and metadata_provider_user != request.user.username') - _logger.debug('metadata_provider_user = %s', self.metadata_provider_user) - _logger.debug('request.user.username = %s', request.user.username) + _logger.debug( + "404 due to state == draft and metadata_provider_user != request.user.username" + ) + _logger.debug("metadata_provider_user = %s", self.metadata_provider_user) + _logger.debug("request.user.username = %s", request.user.username) raise Http404 - if self.editor and 'owner_id' in self.editor: - return request.user.username == self.editor['owner_id'] - elif self.metadata_provider_user: + if self.metadata_provider_user: return request.user.username == self.metadata_provider_user # note: once access control plans evolve, user_created may not be a legit field ever @@ -469,9 +574,12 @@ def user_is_privileged(self, request): :return: """ if request.user.is_service: - if request.method == 'GET': - if not self._check_catalog_permissions(self.data_catalog.catalog_record_group_read, - self.data_catalog.catalog_record_services_read, request): + if request.method == "GET": + if not self._check_catalog_permissions( + self.data_catalog.catalog_record_group_read, + self.data_catalog.catalog_record_services_read, + request, + ): return False else: return True @@ -497,64 +605,85 @@ def _check_catalog_permissions(self, catalog_groups, catalog_services, request=N if request: self.request = request - if not self.request: # pragma: no cover + if not self.request: # pragma: no cover # should only only happen when setting up test cases - assert executing_test_case(), 'only permitted when setting up testing conditions' + assert executing_test_case(), "only permitted when setting up testing conditions" return True if self.request.user.is_service: if catalog_services: - allowed_services = [i.lower() for i in catalog_services.split(',')] + allowed_services = [i.lower() for i in catalog_services.split(",")] from metax_api.services import AuthService - return AuthService.check_services_against_allowed_services(self.request, allowed_services) + + return AuthService.check_services_against_allowed_services( + self.request, allowed_services + ) return False elif not self.request.user.is_service: if catalog_groups: - allowed_groups = catalog_groups.split(',') + allowed_groups = catalog_groups.split(",") from metax_api.services import AuthService + return AuthService.check_user_groups_against_groups(self.request, allowed_groups) return True _logger.info( - 'Catalog {} is not belonging to any service or group '.format(self.data_catalog.catalog_json['identifier']) + "Catalog {} is not belonging to any service or group ".format( + self.data_catalog.catalog_json["identifier"] + ) ) return False def _access_type_is_open(self): from metax_api.services import CatalogRecordService as CRS - return CRS.get_research_dataset_access_type(self.research_dataset) == ACCESS_TYPES['open'] + + return CRS.get_research_dataset_access_type(self.research_dataset) == ACCESS_TYPES["open"] def _access_type_is_login(self): from metax_api.services import CatalogRecordService as CRS - return CRS.get_research_dataset_access_type(self.research_dataset) == ACCESS_TYPES['login'] + + return CRS.get_research_dataset_access_type(self.research_dataset) == ACCESS_TYPES["login"] def _access_type_is_embargo(self): from metax_api.services import CatalogRecordService as CRS - return CRS.get_research_dataset_access_type(self.research_dataset) == ACCESS_TYPES['embargo'] + + return ( + CRS.get_research_dataset_access_type(self.research_dataset) == ACCESS_TYPES["embargo"] + ) def _access_type_is_permit(self): from metax_api.services import CatalogRecordService as CRS - return CRS.get_research_dataset_access_type(self.research_dataset) == ACCESS_TYPES['permit'] + + return CRS.get_research_dataset_access_type(self.research_dataset) == ACCESS_TYPES["permit"] def _access_type_was_permit(self): from metax_api.services import CatalogRecordService as CRS - return CRS.get_research_dataset_access_type(self._initial_data['research_dataset']) == ACCESS_TYPES['permit'] + + return ( + CRS.get_research_dataset_access_type(self._initial_data["research_dataset"]) + == ACCESS_TYPES["permit"] + ) def _embargo_is_available(self): - if not self.research_dataset.get('access_rights', {}).get('available', False): + if not self.research_dataset.get("access_rights", {}).get("available", False): return False try: return get_tz_aware_now_without_micros() >= parse_timestamp_string_to_tz_aware_datetime( - self.research_dataset.get('access_rights', {}).get('available', {})) + self.research_dataset.get("access_rights", {}).get("available", {}) + ) except Exception as e: _logger.error(e) return False def authorized_to_see_catalog_record_files(self, request): - return self.user_is_privileged(request) or self._access_type_is_open() or self._access_type_is_login() or \ - (self._access_type_is_embargo() and self._embargo_is_available()) + return ( + self.user_is_privileged(request) + or self._access_type_is_open() + or self._access_type_is_login() + or (self._access_type_is_embargo() and self._embargo_is_available()) + ) def save(self, *args, **kwargs): """ @@ -562,7 +691,7 @@ def save(self, *args, **kwargs): unknown keyword arguments. """ if self._operation_is_create(): - self._pre_create_operations(pid_type=kwargs.pop('pid_type', None)) + self._pre_create_operations(pid_type=kwargs.pop("pid_type", None)) super(CatalogRecord, self).save(*args, **kwargs) self._post_create_operations() else: @@ -582,42 +711,52 @@ def _process_file_changes(self, file_changes, new_record_id, old_record_id): """ actual_files_changed = False - files_to_add = file_changes['files_to_add'] - files_to_remove = file_changes['files_to_remove'] - files_to_keep = file_changes['files_to_keep'] - dirs_to_add_by_project = file_changes['dirs_to_add_by_project'] - dirs_to_remove_by_project = file_changes['dirs_to_remove_by_project'] - dirs_to_keep_by_project = file_changes['dirs_to_keep_by_project'] - - if files_to_add or files_to_remove or files_to_keep or dirs_to_add_by_project or \ - dirs_to_remove_by_project or dirs_to_keep_by_project: + files_to_add = file_changes["files_to_add"] + files_to_remove = file_changes["files_to_remove"] + files_to_keep = file_changes["files_to_keep"] + dirs_to_add_by_project = file_changes["dirs_to_add_by_project"] + dirs_to_remove_by_project = file_changes["dirs_to_remove_by_project"] + dirs_to_keep_by_project = file_changes["dirs_to_keep_by_project"] + + if ( + files_to_add + or files_to_remove + or files_to_keep + or dirs_to_add_by_project + or dirs_to_remove_by_project + or dirs_to_keep_by_project + ): # note: files_to_keep and dirs_to_keep_by_project are also included because we # want to create new version on some cumulative_state changes. if DEBUG: - _logger.debug('Detected the following file changes:') + _logger.debug("Detected the following file changes:") if files_to_keep: # sql to copy single files from the previous version to the new version. only copy those # files which have been listed in research_dataset.files - sql_copy_files_from_prev_version = ''' + sql_copy_files_from_prev_version = """ insert into metax_api_catalogrecord_files (catalogrecord_id, file_id) select %s as catalogrecord_id, file_id from metax_api_catalogrecord_files as cr_f inner join metax_api_file as f on f.id = cr_f.file_id where catalogrecord_id = %s and file_id in %s - ''' - sql_params_copy_files = [new_record_id, old_record_id, tuple(files_to_keep)] + """ + sql_params_copy_files = [ + new_record_id, + old_record_id, + tuple(files_to_keep), + ] if DEBUG: - _logger.debug('File ids to keep: %s' % files_to_keep) + _logger.debug("File ids to keep: %s" % files_to_keep) if dirs_to_keep_by_project: # sql top copy files from entire directories. only copy files from the upper level dirs found # by processing research_dataset.directories. - sql_copy_dirs_from_prev_version = ''' + sql_copy_dirs_from_prev_version = """ insert into metax_api_catalogrecord_files (catalogrecord_id, file_id) select %s as catalogrecord_id, file_id from metax_api_catalogrecord_files as cr_f @@ -631,21 +770,22 @@ def _process_file_changes(self, file_changes, new_record_id, old_record_id): metax_api_catalogrecord_files cr_f where catalogrecord_id = %s ) - ''' + """ sql_params_copy_dirs = [new_record_id, old_record_id] copy_dirs_sql = [] for project, dir_paths in dirs_to_keep_by_project.items(): for dir_path in dir_paths: - copy_dirs_sql.append("(f.project_identifier = %s and f.file_path like (%s || '/%%'))") + copy_dirs_sql.append( + "(f.project_identifier = %s and f.file_path like (%s || '/%%'))" + ) sql_params_copy_dirs.extend([project, dir_path]) sql_params_copy_dirs.extend([new_record_id]) sql_copy_dirs_from_prev_version = sql_copy_dirs_from_prev_version.replace( - 'COMPARE_PROJECT_AND_FILE_PATHS', - ' or '.join(copy_dirs_sql) + "COMPARE_PROJECT_AND_FILE_PATHS", " or ".join(copy_dirs_sql) ) # ^ generates: # and ( @@ -657,17 +797,17 @@ def _process_file_changes(self, file_changes, new_record_id, old_record_id): # ) if DEBUG: - _logger.debug('Directory paths to keep, by project:') + _logger.debug("Directory paths to keep, by project:") for project, dir_paths in dirs_to_keep_by_project.items(): - _logger.debug('\tProject: %s' % project) + _logger.debug("\tProject: %s" % project) for dir_path in dir_paths: - _logger.debug('\t\t%s' % dir_path) + _logger.debug("\t\t%s" % dir_path) if dirs_to_add_by_project: # sql to add new files by directory path that were not previously included. # also takes care of "path is already included by another dir, but i want to check if there # are new files to add in there" - sql_select_and_insert_files_by_dir_path = ''' + sql_select_and_insert_files_by_dir_path = """ insert into metax_api_catalogrecord_files (catalogrecord_id, file_id) select %s as catalogrecord_id, f.id from metax_api_file as f @@ -680,36 +820,39 @@ def _process_file_changes(self, file_changes, new_record_id, old_record_id): metax_api_catalogrecord_files cr_f where catalogrecord_id = %s ) - ''' + """ sql_params_insert_dirs = [new_record_id] add_dirs_sql = [] for project, dir_paths in dirs_to_add_by_project.items(): for dir_path in dir_paths: - add_dirs_sql.append("(f.project_identifier = %s and f.file_path like (%s || '/%%'))") + add_dirs_sql.append( + "(f.project_identifier = %s and f.file_path like (%s || '/%%'))" + ) sql_params_insert_dirs.extend([project, dir_path]) - sql_select_and_insert_files_by_dir_path = sql_select_and_insert_files_by_dir_path.replace( - 'COMPARE_PROJECT_AND_FILE_PATHS', - ' or '.join(add_dirs_sql) + sql_select_and_insert_files_by_dir_path = ( + sql_select_and_insert_files_by_dir_path.replace( + "COMPARE_PROJECT_AND_FILE_PATHS", " or ".join(add_dirs_sql) + ) ) sql_params_insert_dirs.extend([new_record_id]) if DEBUG: - _logger.debug('Directory paths to add, by project:') + _logger.debug("Directory paths to add, by project:") for project, dir_paths in dirs_to_add_by_project.items(): - _logger.debug('\tProject: %s' % project) + _logger.debug("\tProject: %s" % project) for dir_path in dir_paths: - _logger.debug('\t\t%s' % dir_path) + _logger.debug("\t\t%s" % dir_path) if files_to_add: # sql to add any new singular files which were not covered by any directory path # being added. also takes care of "path is already included by another dir, # but this file did not necessarily exist yet at that time, so add it in case # its a new file" - sql_insert_single_files = ''' + sql_insert_single_files = """ insert into metax_api_catalogrecord_files (catalogrecord_id, file_id) select %s as catalogrecord_id, f.id from metax_api_file as f @@ -720,13 +863,17 @@ def _process_file_changes(self, file_changes, new_record_id, old_record_id): metax_api_catalogrecord_files cr_f where catalogrecord_id = %s ) - ''' - sql_params_insert_single = [new_record_id, tuple(files_to_add), new_record_id] + """ + sql_params_insert_single = [ + new_record_id, + tuple(files_to_add), + new_record_id, + ] if DEBUG: - _logger.debug('File ids to add: %s' % files_to_add) + _logger.debug("File ids to add: %s" % files_to_add) - sql_detect_files_changed = ''' + sql_detect_files_changed = """ select exists( select a.file_id from metax_api_catalogrecord_files a where a.catalogrecord_id = %s except @@ -738,12 +885,12 @@ def _process_file_changes(self, file_changes, new_record_id, old_record_id): except select b.file_id from metax_api_catalogrecord_files b where b.catalogrecord_id = %s ) as compare_old_to_new - ''' + """ sql_params_files_changed = [ new_record_id, old_record_id, old_record_id, - new_record_id + new_record_id, ] with connection.cursor() as cr: @@ -765,7 +912,9 @@ def _process_file_changes(self, file_changes, new_record_id, old_record_id): actual_files_changed = any(v[0] for v in cr.fetchall()) if DEBUG: - _logger.debug('Actual files changed during version change: %s' % str(actual_files_changed)) + _logger.debug( + "Actual files changed during version change: %s" % str(actual_files_changed) + ) else: # no files to specifically add or remove - do nothing. shouldnt even be here. the new record # created will be discarded @@ -785,14 +934,15 @@ def _find_file_changes(self): # not allowing file/dir updates via rest api for deprecated datasets will (hopefully) make # the versioning more robust if self.deprecated and any( - file_description_changes['files']['removed'] or - file_description_changes['files']['added'] or - file_description_changes['directories']['removed'] or - file_description_changes['directories']['added']): + file_description_changes["files"]["removed"] + or file_description_changes["files"]["added"] + or file_description_changes["directories"]["removed"] + or file_description_changes["directories"]["added"] + ): raise Http400( - 'Cannot add or remove files/directories from deprecated dataset. ' - 'Please use API /rpc/fix_deprecated to fix deprecation and to allow file modifications.' + "Cannot add or remove files/directories from deprecated dataset. " + "Please use API /rpc/fix_deprecated to fix deprecation and to allow file modifications." ) # after copying files from the previous version to the new version, these arrays hold any new @@ -820,146 +970,190 @@ def _find_file_changes(self): # permissions for changed files: no need to retrieve related project identifiers again changed_projects = defaultdict(set) - self._find_new_dirs_to_add(file_description_changes, - dirs_to_add_by_project, - dirs_to_remove_by_project, - dirs_to_keep_by_project) + self._find_new_dirs_to_add( + file_description_changes, + dirs_to_add_by_project, + dirs_to_remove_by_project, + dirs_to_keep_by_project, + ) - self._find_new_files_to_add(file_description_changes, - files_to_add, - files_to_remove, - files_to_keep, - changed_projects) + self._find_new_files_to_add( + file_description_changes, + files_to_add, + files_to_remove, + files_to_keep, + changed_projects, + ) # involved projects from single files (research_dataset.files) were accumulated # in the previous method, but for dirs, its just handy to get the keys from below # variables... for project_identifier in dirs_to_add_by_project.keys(): - changed_projects['files_added'].add(project_identifier) + changed_projects["files_added"].add(project_identifier) for project_identifier in dirs_to_remove_by_project.keys(): - changed_projects['files_removed'].add(project_identifier) + changed_projects["files_removed"].add(project_identifier) return { - 'files_to_add': files_to_add, - 'files_to_remove': files_to_remove, - 'files_to_keep': files_to_keep, - 'dirs_to_add_by_project': dirs_to_add_by_project, - 'dirs_to_remove_by_project': dirs_to_remove_by_project, - 'dirs_to_keep_by_project': dirs_to_keep_by_project, - 'changed_projects': changed_projects, + "files_to_add": files_to_add, + "files_to_remove": files_to_remove, + "files_to_keep": files_to_keep, + "dirs_to_add_by_project": dirs_to_add_by_project, + "dirs_to_remove_by_project": dirs_to_remove_by_project, + "dirs_to_keep_by_project": dirs_to_keep_by_project, + "changed_projects": changed_projects, } - def _find_new_dirs_to_add(self, file_description_changes, dirs_to_add_by_project, dirs_to_remove_by_project, - dirs_to_keep_by_project): + def _find_new_dirs_to_add( + self, + file_description_changes, + dirs_to_add_by_project, + dirs_to_remove_by_project, + dirs_to_keep_by_project, + ): """ Based on changes in research_metadata.directories (parameter file_description_changes), find out which directories should be kept when copying files from the previous version to the new version, and which new directories should be added. """ - assert 'directories' in file_description_changes + assert "directories" in file_description_changes - dir_identifiers = list(file_description_changes['directories']['added']) +\ - list(file_description_changes['directories']['removed']) + dir_identifiers = list(file_description_changes["directories"]["added"]) + list( + file_description_changes["directories"]["removed"] + ) - dir_details = Directory.objects.filter(identifier__in=dir_identifiers) \ - .values('project_identifier', 'identifier', 'directory_path') + dir_details = Directory.objects.filter(identifier__in=dir_identifiers).values( + "project_identifier", "identifier", "directory_path" + ) # skip deprecated datasets, since there might be deleted directories if len(dir_identifiers) != len(dir_details) and not self.deprecated: - existig_dirs = set( d['identifier'] for d in dir_details ) - missing_identifiers = [ d for d in dir_identifiers if d not in existig_dirs ] - raise ValidationError({'detail': ['the following directory identifiers were not found:\n%s' - % '\n'.join(missing_identifiers) ]}) + existig_dirs = set(d["identifier"] for d in dir_details) + missing_identifiers = [d for d in dir_identifiers if d not in existig_dirs] + raise ValidationError( + { + "detail": [ + "the following directory identifiers were not found:\n%s" + % "\n".join(missing_identifiers) + ] + } + ) for dr in dir_details: - if dr['identifier'] in file_description_changes['directories']['added']: + if dr["identifier"] in file_description_changes["directories"]["added"]: # include all new dirs found, to add files from an entirely new directory, # or to search an already existing directory for new files (and sub dirs). # as a result the actual set of files may or may not change. - dirs_to_add_by_project[dr['project_identifier']].append(dr['directory_path']) + dirs_to_add_by_project[dr["project_identifier"]].append(dr["directory_path"]) - elif dr['identifier'] in file_description_changes['directories']['removed']: - if not self._path_included_in_previous_metadata_version(dr['project_identifier'], dr['directory_path']): + elif dr["identifier"] in file_description_changes["directories"]["removed"]: + if not self._path_included_in_previous_metadata_version( + dr["project_identifier"], dr["directory_path"] + ): # only remove dirs that are not included by other directory paths. # as a result the actual set of files may change, if the path is not included # in the new directory additions - dirs_to_remove_by_project[dr['project_identifier']].append(dr['directory_path']) + dirs_to_remove_by_project[dr["project_identifier"]].append(dr["directory_path"]) # when keeping directories when copying, only the top level dirs are required top_dirs_by_project = self._get_top_level_parent_dirs_by_project( - file_description_changes['directories']['keep']) + file_description_changes["directories"]["keep"] + ) for project, dirs in top_dirs_by_project.items(): dirs_to_keep_by_project[project] = dirs - def _find_new_files_to_add(self, file_description_changes, files_to_add, files_to_remove, files_to_keep, - changed_projects): + def _find_new_files_to_add( + self, + file_description_changes, + files_to_add, + files_to_remove, + files_to_keep, + changed_projects, + ): """ Based on changes in research_metadata.files (parameter file_description_changes), find out which files should be kept when copying files from the previous version to the new version, and which new files should be added. """ - assert 'files' in file_description_changes + assert "files" in file_description_changes - add_and_keep_ids = list(file_description_changes['files']['added']) \ - + list(file_description_changes['files']['keep']) + add_and_keep_ids = list(file_description_changes["files"]["added"]) + list( + file_description_changes["files"]["keep"] + ) - add_and_keep = File.objects.filter(identifier__in=add_and_keep_ids) \ - .values('id', 'project_identifier', 'identifier', 'file_path') + add_and_keep = File.objects.filter(identifier__in=add_and_keep_ids).values( + "id", "project_identifier", "identifier", "file_path" + ) - removed_ids = list(file_description_changes['files']['removed']) + removed_ids = list(file_description_changes["files"]["removed"]) - removed = File.objects_unfiltered.filter(identifier__in=removed_ids) \ - .values('id', 'project_identifier', 'identifier', 'file_path') + removed = File.objects_unfiltered.filter(identifier__in=removed_ids).values( + "id", "project_identifier", "identifier", "file_path" + ) file_details = add_and_keep | removed if len(add_and_keep_ids) + len(removed_ids) != len(file_details) and not self.deprecated: - existig_files = set( f['identifier'] for f in file_details ) - missing_identifiers = [ f for f in add_and_keep_ids if f not in existig_files ] - missing_identifiers += [ f for f in removed_ids if f not in existig_files ] - raise ValidationError({'detail': ['the following file identifiers were not found:\n%s' - % '\n'.join(missing_identifiers) ]}) + existig_files = set(f["identifier"] for f in file_details) + missing_identifiers = [f for f in add_and_keep_ids if f not in existig_files] + missing_identifiers += [f for f in removed_ids if f not in existig_files] + raise ValidationError( + { + "detail": [ + "the following file identifiers were not found:\n%s" + % "\n".join(missing_identifiers) + ] + } + ) for f in file_details: - if f['identifier'] in file_description_changes['files']['added']: + if f["identifier"] in file_description_changes["files"]["added"]: # include all new files even if it already included by another directory's path, # to check later that it is not a file that was created later. # as a result the actual set of files may or may not change. - files_to_add.append(f['id']) - changed_projects['files_added'].add(f['project_identifier']) - elif f['identifier'] in file_description_changes['files']['removed']: - if not self._path_included_in_previous_metadata_version(f['project_identifier'], f['file_path']): + files_to_add.append(f["id"]) + changed_projects["files_added"].add(f["project_identifier"]) + elif f["identifier"] in file_description_changes["files"]["removed"]: + if not self._path_included_in_previous_metadata_version( + f["project_identifier"], f["file_path"] + ): # file is being removed. # path is not included by other directories in the previous version, # which means the actual set of files may change, if the path is not included # in the new directory additions - files_to_remove.append(f['id']) - changed_projects['files_removed'].add(f['project_identifier']) - elif f['identifier'] in file_description_changes['files']['keep']: - files_to_keep.append(f['id']) + files_to_remove.append(f["id"]) + changed_projects["files_removed"].add(f["project_identifier"]) + elif f["identifier"] in file_description_changes["files"]["keep"]: + files_to_keep.append(f["id"]) def _path_included_in_previous_metadata_version(self, project, path): """ Check if a path in a specific project is already included in the path of another directory included in the PREVIOUS VERSION dataset selected directories. """ - if not hasattr(self, '_previous_highest_level_dirs_by_project'): - if 'directories' not in self._initial_data['research_dataset']: + if not hasattr(self, "_previous_highest_level_dirs_by_project"): + if "directories" not in self._initial_data["research_dataset"]: return False dir_identifiers = [ - d['identifier'] for d in self._initial_data['research_dataset']['directories'] + d["identifier"] for d in self._initial_data["research_dataset"]["directories"] ] - self._previous_highest_level_dirs_by_project = self._get_top_level_parent_dirs_by_project(dir_identifiers) + self._previous_highest_level_dirs_by_project = ( + self._get_top_level_parent_dirs_by_project(dir_identifiers) + ) return any( - True for dr_path in self._previous_highest_level_dirs_by_project.get(project, []) - if path != dr_path and path.startswith('%s/' % dr_path) + True + for dr_path in self._previous_highest_level_dirs_by_project.get(project, []) + if path != dr_path and path.startswith("%s/" % dr_path) ) def delete(self, *args, **kwargs): + if kwargs.get("hard"): + super().delete() + return self.id + if self.state == self.STATE_DRAFT: - _logger.info('Deleting draft dataset %s permanently' % self.identifier) + _logger.info("Deleting draft dataset %s permanently" % self.identifier) if self.previous_dataset_version: self.previous_dataset_version.next_dataset_version = None @@ -972,24 +1166,25 @@ def delete(self, *args, **kwargs): elif self.state == self.STATE_PUBLISHED: if self.has_alternate_records(): self._remove_from_alternate_record_set() - if is_metax_generated_doi_identifier(self.research_dataset['preferred_identifier']): - self.add_post_request_callable(DataciteDOIUpdate(self, self.research_dataset['preferred_identifier'], - 'delete')) + if is_metax_generated_doi_identifier(self.research_dataset["preferred_identifier"]): + self.add_post_request_callable( + DataciteDOIUpdate(self, self.research_dataset["preferred_identifier"], "delete") + ) - if self._dataset_has_rems_managed_access() and settings.REMS['ENABLED']: - self._pre_rems_deletion('dataset deletion') - super().save(update_fields=['rems_identifier', 'access_granter']) + if self._dataset_has_rems_managed_access() and settings.REMS["ENABLED"]: + self._pre_rems_deletion("dataset deletion") + super().save(update_fields=["rems_identifier", "access_granter"]) - self.add_post_request_callable(RabbitMQPublishRecord(self, 'delete')) + self.add_post_request_callable(RabbitMQPublishRecord(self, "delete")) log_args = { - 'event': 'dataset_deleted', - 'user_id': self.user_modified, - 'catalogrecord': { - 'identifier': self.identifier, - 'preferred_identifier': self.preferred_identifier, - 'data_catalog': self.data_catalog.catalog_json['identifier'], - } + "event": "dataset_deleted", + "user_id": self.user_modified, + "catalogrecord": { + "identifier": self.identifier, + "preferred_identifier": self.preferred_identifier, + "data_catalog": self.data_catalog.catalog_json["identifier"], + }, } if self.catalog_is_legacy(): # delete permanently instead of only marking as 'removed' @@ -998,8 +1193,8 @@ def delete(self, *args, **kwargs): return crid else: super().remove(*args, **kwargs) - log_args['catalogrecord']['date_removed'] = datetime_to_str(self.date_removed) - log_args['catalogrecord']['date_modified'] = datetime_to_str(self.date_modified) + log_args["catalogrecord"]["date_removed"] = datetime_to_str(self.date_removed) + log_args["catalogrecord"]["date_modified"] = datetime_to_str(self.date_modified) self.add_post_request_callable(DelayedLog(**log_args)) return self.id @@ -1008,26 +1203,28 @@ def deprecate(self, timestamp=None): self.deprecated = True self.date_deprecated = self.date_modified = timestamp or get_tz_aware_now_without_micros() - if self._dataset_has_rems_managed_access() and settings.REMS['ENABLED']: - self._pre_rems_deletion('dataset deprecation') - super().save(update_fields=['rems_identifier', 'access_granter']) + if self._dataset_has_rems_managed_access() and settings.REMS["ENABLED"]: + self._pre_rems_deletion("dataset deprecation") + super().save(update_fields=["rems_identifier", "access_granter"]) - super().save(update_fields=['deprecated', 'date_deprecated', 'date_modified']) - self.add_post_request_callable(DelayedLog( - event='dataset_deprecated', - catalogrecord={ - 'identifier': self.identifier, - 'date_deprecated': datetime_to_str(self.date_deprecated), - } - )) + super().save(update_fields=["deprecated", "date_deprecated", "date_modified"]) + self.add_post_request_callable( + DelayedLog( + event="dataset_deprecated", + catalogrecord={ + "identifier": self.identifier, + "date_deprecated": datetime_to_str(self.date_deprecated), + }, + ) + ) @property def identifiers_dict(self): try: return { - 'id': self.id, - 'identifier': self.identifier, - 'preferred_identifier': self.research_dataset['preferred_identifier'], + "id": self.id, + "identifier": self.identifier, + "preferred_identifier": self.research_dataset["preferred_identifier"], } except: return {} @@ -1035,14 +1232,14 @@ def identifiers_dict(self): @property def preferred_identifier(self): try: - return self.research_dataset['preferred_identifier'] + return self.research_dataset["preferred_identifier"] except: return None @property def metadata_version_identifier(self): try: - return self.research_dataset['metadata_version_identifier'] + return self.research_dataset["metadata_version_identifier"] except: return None @@ -1053,32 +1250,38 @@ def preservation_dataset_origin_version_exists(self): attribute does not exist on the instance at all, until the field has a proper value. Django weirdness? """ - return hasattr(self, 'preservation_dataset_origin_version') + return hasattr(self, "preservation_dataset_origin_version") def catalog_versions_datasets(self): - return self.data_catalog.catalog_json.get('dataset_versioning', False) is True + return self.data_catalog.catalog_json.get("dataset_versioning", False) is True def catalog_is_harvested(self): - return self.data_catalog.catalog_json.get('harvested', False) is True + return self.data_catalog.catalog_json.get("harvested", False) is True def catalog_is_legacy(self): - return self.data_catalog.catalog_json['identifier'] in LEGACY_CATALOGS + return self.data_catalog.catalog_json["identifier"] in LEGACY_CATALOGS def catalog_is_ida(self, data=None): if data: - return DataCatalog.objects.get(pk=data['data_catalog_id']).catalog_json['identifier'] == IDA_CATALOG - return self.data_catalog.catalog_json['identifier'] == IDA_CATALOG + return ( + DataCatalog.objects.get(pk=data["data_catalog_id"]).catalog_json["identifier"] + == IDA_CATALOG + ) + return self.data_catalog.catalog_json["identifier"] == IDA_CATALOG def catalog_is_att(self, data=None): if data: - return DataCatalog.objects.get(pk=data['data_catalog_id']).catalog_json['identifier'] == ATT_CATALOG - return self.data_catalog.catalog_json['identifier'] == ATT_CATALOG + return ( + DataCatalog.objects.get(pk=data["data_catalog_id"]).catalog_json["identifier"] + == ATT_CATALOG + ) + return self.data_catalog.catalog_json["identifier"] == ATT_CATALOG def catalog_is_pas(self): - return self.data_catalog.catalog_json['identifier'] == PAS_CATALOG + return self.data_catalog.catalog_json["identifier"] == PAS_CATALOG def catalog_is_dft(self): - return self.data_catalog.catalog_json['identifier'] == DFT_CATALOG + return self.data_catalog.catalog_json["identifier"] == DFT_CATALOG def is_published(self): return self.state == self.STATE_PUBLISHED @@ -1092,21 +1295,23 @@ def _save_as_draft(self): def _generate_issued_date(self): if not (self.catalog_is_harvested()): - if 'issued' not in self.research_dataset: + if "issued" not in self.research_dataset: current_time = get_tz_aware_now_without_micros() - self.research_dataset['issued'] = datetime_to_str(current_time)[0:10] + self.research_dataset["issued"] = datetime_to_str(current_time)[0:10] def get_metadata_version_listing(self): entries = [] for entry in self.research_dataset_versions.all(): - entries.append({ - 'id': entry.id, - 'date_created': entry.date_created, - 'metadata_version_identifier': entry.metadata_version_identifier, - }) + entries.append( + { + "id": entry.id, + "date_created": entry.date_created, + "metadata_version_identifier": entry.metadata_version_identifier, + } + ) if entry.stored_to_pas: # dont include null values - entries[-1]['stored_to_pas'] = entry.stored_to_pas + entries[-1]["stored_to_pas"] = entry.stored_to_pas return entries def _get_user_info_for_rems(self): @@ -1116,23 +1321,23 @@ def _get_user_info_for_rems(self): if self.request.user.is_service: # use constant keys for easier validation user_info = { - 'userid': self.access_granter.get('userid'), - 'name': self.access_granter.get('name'), - 'email': self.access_granter.get('email') + "userid": self.access_granter.get("userid"), + "name": self.access_granter.get("name"), + "email": self.access_granter.get("email"), } else: # end user api user_info = { - 'userid': self.request.user.token.get('CSCUserName'), - 'name': self.request.user.token.get('displayName'), - 'email': self.request.user.token.get('email') + "userid": self.request.user.token.get("CSCUserName"), + "name": self.request.user.token.get("displayName"), + "email": self.request.user.token.get("email"), } if any([v is None for v in user_info.values()]): - raise Http400('Could not find the needed user information for REMS') + raise Http400("Could not find the needed user information for REMS") if not all([isinstance(v, str) for v in user_info.values()]): - raise Http400('user information fields must be string') + raise Http400("user information fields must be string") return user_info @@ -1140,32 +1345,38 @@ def _validate_for_rems(self): """ Ensures that all necessary information for REMS access """ - if self._access_type_is_permit() and not self.research_dataset['access_rights'].get('license', False): - raise Http400('You must define license for dataset in order to make it REMS manageable') + if self._access_type_is_permit() and not self.research_dataset["access_rights"].get( + "license", False + ): + raise Http400("You must define license for dataset in order to make it REMS manageable") if self.request.user.is_service and not self.access_granter: - raise Http400('Missing access_granter') + raise Http400("Missing access_granter") def _assert_api_version(self): if not self.api_meta: # This should be possible only for test data - _logger.warning(f'no api_meta found for {self.identifier}') + _logger.warning(f"no api_meta found for {self.identifier}") return - if not self.api_meta['version'] == self.api_version: - raise Http400('Please use the correct api version to edit this dataset') + if not self.api_meta["version"] == self.api_version: + raise Http400("Please use the correct api version to edit this dataset") def _set_api_version(self): # TODO: Can possibly be deleted when v1 api is removed from use and all # datasets have been migrated to v2 - self.api_meta['version'] = self.api_version + self.api_meta["version"] = self.api_version def _pre_create_operations(self, pid_type=None): - if not self._check_catalog_permissions(self.data_catalog.catalog_record_group_create, - self.data_catalog.catalog_record_services_create): - raise Http403({ 'detail': [ 'You are not permitted to create datasets in this data catalog.' ]}) + if not self._check_catalog_permissions( + self.data_catalog.catalog_record_group_create, + self.data_catalog.catalog_record_services_create, + ): + raise Http403( + {"detail": ["You are not permitted to create datasets in this data catalog."]} + ) - self.research_dataset['metadata_version_identifier'] = generate_uuid_identifier() + self.research_dataset["metadata_version_identifier"] = generate_uuid_identifier() self.identifier = generate_uuid_identifier() if self.catalog_is_pas(): @@ -1180,56 +1391,66 @@ def _pre_create_operations(self, pid_type=None): pass elif self.catalog_is_legacy(): - if 'preferred_identifier' not in self.research_dataset: - raise ValidationError({ - 'detail': [ - 'Selected catalog %s is a legacy catalog. Preferred identifiers are not ' - 'automatically generated for datasets stored in legacy catalogs, nor is ' - 'their uniqueness enforced. Please provide a value for dataset field ' - 'preferred_identifier.' - % self.data_catalog.catalog_json['identifier'] - ] - }) + if "preferred_identifier" not in self.research_dataset: + raise ValidationError( + { + "detail": [ + "Selected catalog %s is a legacy catalog. Preferred identifiers are not " + "automatically generated for datasets stored in legacy catalogs, nor is " + "their uniqueness enforced. Please provide a value for dataset field " + "preferred_identifier." % self.data_catalog.catalog_json["identifier"] + ] + } + ) _logger.info( - 'Catalog %s is a legacy catalog - not generating pid' - % self.data_catalog.catalog_json['identifier'] + "Catalog %s is a legacy catalog - not generating pid" + % self.data_catalog.catalog_json["identifier"] ) elif self._save_as_draft(): self.state = self.STATE_DRAFT - self.research_dataset['preferred_identifier'] = 'draft:%s' % self.identifier + self.research_dataset["preferred_identifier"] = "draft:%s" % self.identifier if self._get_preferred_identifier_type_from_request() == IdentifierType.DOI: self.use_doi_for_published = True else: self.use_doi_for_published = False else: if pref_id_type == IdentifierType.URN: - self.research_dataset['preferred_identifier'] = generate_uuid_identifier(urn_prefix=True) + self.research_dataset["preferred_identifier"] = generate_uuid_identifier( + urn_prefix=True + ) elif pref_id_type == IdentifierType.DOI: if not (self.catalog_is_ida() or self.catalog_is_pas()): raise Http400("Cannot create DOI for other than datasets in IDA or PAS catalog") - _logger.debug('pref_id_type == %s, generating doi' % pref_id_type) + _logger.debug("pref_id_type == %s, generating doi" % pref_id_type) doi_id = generate_doi_identifier() - self.research_dataset['preferred_identifier'] = doi_id + self.research_dataset["preferred_identifier"] = doi_id self.preservation_identifier = doi_id else: - _logger.debug("Identifier type not specified in the request. Using URN identifier for pref id") - self.research_dataset['preferred_identifier'] = generate_uuid_identifier(urn_prefix=True) + _logger.debug( + "Identifier type not specified in the request. Using URN identifier for pref id" + ) + self.research_dataset["preferred_identifier"] = generate_uuid_identifier( + urn_prefix=True + ) if not self.metadata_owner_org: # field metadata_owner_org is optional, but must be set. in case it is omitted, # derive from metadata_provider_org. self.metadata_owner_org = self.metadata_provider_org - if 'remote_resources' in self.research_dataset: + if "remote_resources" in self.research_dataset: self._calculate_total_remote_resources_byte_size() + if not ("files" in self.research_dataset or "directories" in self.research_dataset) and "total_files_byte_size" in self.research_dataset: + self.research_dataset.pop("total_files_byte_size") + if self.cumulative_state == self.CUMULATIVE_STATE_CLOSED: - raise Http400('Cannot create cumulative dataset with state closed') + raise Http400("Cannot create cumulative dataset with state closed") elif self.cumulative_state == self.CUMULATIVE_STATE_YES: if self.preservation_state > self.PRESERVATION_STATE_INITIALIZED: - raise Http400('Dataset cannot be cumulative if it is in PAS process') + raise Http400("Dataset cannot be cumulative if it is in PAS process") self.date_cumulation_started = self.date_created @@ -1238,12 +1459,12 @@ def _pre_create_operations(self, pid_type=None): self._set_api_version() def _post_create_operations(self): - if 'files' in self.research_dataset or 'directories' in self.research_dataset: + if "files" in self.research_dataset or "directories" in self.research_dataset: # files must be added after the record itself has been created, to be able # to insert into a many2many relation. self.files.add(*self._get_dataset_selected_file_ids()) self._calculate_total_files_byte_size() - super().save(update_fields=['research_dataset']) # save byte size calculation + super().save(update_fields=["research_dataset"]) # save byte size calculation self.calculate_directory_byte_sizes_and_file_counts() if self.cumulative_state == self.CUMULATIVE_STATE_YES: @@ -1265,138 +1486,153 @@ def _post_create_operations(self): dvs.save() dvs.records.add(self) - if (get_identifier_type(self.preferred_identifier) == IdentifierType.DOI or - self.use_doi_for_published is True): + if ( + get_identifier_type(self.preferred_identifier) == IdentifierType.DOI + or self.use_doi_for_published is True + ): self._validate_cr_against_datacite_schema() - if is_metax_generated_doi_identifier(self.research_dataset['preferred_identifier']): - self.add_post_request_callable(DataciteDOIUpdate(self, - self.research_dataset['preferred_identifier'], 'create')) + if is_metax_generated_doi_identifier(self.research_dataset["preferred_identifier"]): + self.add_post_request_callable( + DataciteDOIUpdate(self, self.research_dataset["preferred_identifier"], "create") + ) - if self._dataset_has_rems_managed_access() and settings.REMS['ENABLED']: + if self._dataset_has_rems_managed_access() and settings.REMS["ENABLED"]: self._pre_rems_creation() - super().save(update_fields=['rems_identifier', 'access_granter']) + super().save(update_fields=["rems_identifier", "access_granter"]) super().save() - self.add_post_request_callable(RabbitMQPublishRecord(self, 'create')) + self.add_post_request_callable(RabbitMQPublishRecord(self, "create")) _logger.info( - 'Created a new ' - % (self.id, self.identifier, self.preferred_identifier) + "Created a new " % (self.id, self.identifier, self.preferred_identifier) ) log_args = { - 'catalogrecord': { - 'identifier': self.identifier, - 'preferred_identifier': self.preferred_identifier, - 'data_catalog': self.data_catalog.catalog_json['identifier'], - 'date_created': datetime_to_str(self.date_created), - 'metadata_owner_org': self.metadata_owner_org, + "catalogrecord": { + "identifier": self.identifier, + "preferred_identifier": self.preferred_identifier, + "data_catalog": self.data_catalog.catalog_json["identifier"], + "date_created": datetime_to_str(self.date_created), + "metadata_owner_org": self.metadata_owner_org, }, - 'user_id': self.user_created or self.service_created, + "user_id": self.user_created or self.service_created, } if self.previous_dataset_version: - log_args['event'] = 'dataset_version_created' - log_args['catalogrecord']['previous_version_preferred_identifier'] \ - = self.previous_dataset_version.preferred_identifier + log_args["event"] = "dataset_version_created" + log_args["catalogrecord"][ + "previous_version_preferred_identifier" + ] = self.previous_dataset_version.preferred_identifier else: - log_args['event'] = 'dataset_created' + log_args["event"] = "dataset_created" self.add_post_request_callable(DelayedLog(**log_args)) def _pre_update_operations(self): - if not self._check_catalog_permissions(self.data_catalog.catalog_record_group_edit, - self.data_catalog.catalog_record_services_edit): - raise Http403({ 'detail': [ 'You are not permitted to edit datasets in this data catalog.' ]}) + if not self._check_catalog_permissions( + self.data_catalog.catalog_record_group_edit, + self.data_catalog.catalog_record_services_edit, + ): + raise Http403( + {"detail": ["You are not permitted to edit datasets in this data catalog."]} + ) - if self.field_changed('api_meta'): - self.api_meta = self._initial_data['api_meta'] + if self.field_changed("api_meta"): + self.api_meta = self._initial_data["api_meta"] # possibly raises 400 self._assert_api_version() - if self.field_changed('identifier'): + if self.field_changed("identifier"): # read-only - self.identifier = self._initial_data['identifier'] + self.identifier = self._initial_data["identifier"] - if self.field_changed('research_dataset.metadata_version_identifier'): + if self.field_changed("research_dataset.metadata_version_identifier"): # read-only - self.research_dataset['metadata_version_identifier'] = \ - self._initial_data['research_dataset']['metadata_version_identifier'] + self.research_dataset["metadata_version_identifier"] = self._initial_data[ + "research_dataset" + ]["metadata_version_identifier"] - if self.field_changed('research_dataset.preferred_identifier'): + if self.field_changed("research_dataset.preferred_identifier"): if not (self.catalog_is_harvested() or self.catalog_is_legacy()): - raise Http400("Cannot change preferred_identifier in datasets in non-harvested catalogs") + raise Http400( + "Cannot change preferred_identifier in datasets in non-harvested catalogs" + ) - if self.field_changed('research_dataset.total_files_byte_size'): + if self.field_changed("research_dataset.total_files_byte_size"): # read-only - if 'total_files_byte_size' in self._initial_data['research_dataset']: - self.research_dataset['total_files_byte_size'] = \ - self._initial_data['research_dataset']['total_files_byte_size'] + if "total_files_byte_size" in self._initial_data["research_dataset"]: + self.research_dataset["total_files_byte_size"] = self._initial_data[ + "research_dataset" + ]["total_files_byte_size"] else: - self.research_dataset.pop('total_files_byte_size') + self.research_dataset.pop("total_files_byte_size") - if self.field_changed('research_dataset.total_remote_resources_byte_size'): + if self.field_changed("research_dataset.total_remote_resources_byte_size"): # read-only - if 'total_remote_resources_byte_size' in self._initial_data['research_dataset']: - self.research_dataset['total_remote_resources_byte_size'] = \ - self._initial_data['research_dataset']['total_remote_resources_byte_size'] + if "total_remote_resources_byte_size" in self._initial_data["research_dataset"]: + self.research_dataset["total_remote_resources_byte_size"] = self._initial_data[ + "research_dataset" + ]["total_remote_resources_byte_size"] else: - self.research_dataset.pop('total_remote_resources_byte_size') + self.research_dataset.pop("total_remote_resources_byte_size") - if self.field_changed('preservation_state'): + if self.field_changed("preservation_state"): if self.cumulative_state == self.CUMULATIVE_STATE_YES: - raise Http400('Changing preservation state is not allowed while dataset cumulation is active') + raise Http400( + "Changing preservation state is not allowed while dataset cumulation is active" + ) self._handle_preservation_state_changed() - if self.field_changed('deprecated') and self._initial_data['deprecated'] is True: + if self.field_changed("deprecated") and self._initial_data["deprecated"] is True: raise Http400("Cannot change dataset deprecation state from true to false") - if self.field_changed('date_deprecated') and self._initial_data['date_deprecated']: + if self.field_changed("date_deprecated") and self._initial_data["date_deprecated"]: raise Http400("Cannot change dataset deprecation date when it has been once set") - if self.field_changed('preservation_identifier'): - self.preservation_identifier = self._initial_data['preservation_identifier'] + if self.field_changed("preservation_identifier"): + self.preservation_identifier = self._initial_data["preservation_identifier"] if not self.metadata_owner_org: # can not be updated to null - self.metadata_owner_org = self._initial_data['metadata_owner_org'] + self.metadata_owner_org = self._initial_data["metadata_owner_org"] - if self.field_changed('metadata_provider_org'): + if self.field_changed("metadata_provider_org"): # read-only after creating - self.metadata_provider_org = self._initial_data['metadata_provider_org'] + self.metadata_provider_org = self._initial_data["metadata_provider_org"] - if self.field_changed('metadata_provider_user'): + if self.field_changed("metadata_provider_user"): # read-only after creating - self.metadata_provider_user = self._initial_data['metadata_provider_user'] + self.metadata_provider_user = self._initial_data["metadata_provider_user"] - if self.field_changed('data_catalog_id'): + if self.field_changed("data_catalog_id"): if self.catalog_is_att(self._initial_data) and self.catalog_is_ida(): - self.research_dataset.pop('remote_resources') - self.research_dataset.pop('total_remote_resources_byte_size') + self.research_dataset.pop("remote_resources") + self.research_dataset.pop("total_remote_resources_byte_size") self._handle_metadata_versioning() - if settings.REMS['ENABLED']: + if settings.REMS["ENABLED"]: self._pre_update_handle_rems() - if self.field_changed('research_dataset'): + if self.field_changed("research_dataset"): if self.preservation_state in ( - self.PRESERVATION_STATE_INVALID_METADATA, # 40 - self.PRESERVATION_STATE_METADATA_VALIDATION_FAILED, # 50 - self.PRESERVATION_STATE_VALID_METADATA): # 70 + self.PRESERVATION_STATE_INVALID_METADATA, # 40 + self.PRESERVATION_STATE_METADATA_VALIDATION_FAILED, # 50 + self.PRESERVATION_STATE_VALID_METADATA, + ): # 70 # notifies the user in Hallintaliittyma that the metadata needs to be re-validated - self.preservation_state = self.PRESERVATION_STATE_VALIDATED_METADATA_UPDATED # 60 + self.preservation_state = self.PRESERVATION_STATE_VALIDATED_METADATA_UPDATED # 60 self.preservation_state_modified = self.date_modified self.update_datacite = True else: self.update_datacite = False - if self.field_changed('cumulative_state'): + if self.field_changed("cumulative_state"): raise Http400( "Cannot change cumulative state using REST API. " "use API /rpc/datasets/change_cumulative_state to change cumulative state." @@ -1405,13 +1641,14 @@ def _pre_update_operations(self): if self.catalog_is_pas(): actual_files_changed, _ = self._files_changed() if actual_files_changed: - _logger.info('File changes detected in PAS catalog dataset - aborting') - raise Http400({ 'detail': ['File changes not permitted in PAS catalog' ]}) + _logger.info("File changes detected in PAS catalog dataset - aborting") + raise Http400({"detail": ["File changes not permitted in PAS catalog"]}) - if self.catalog_versions_datasets() and \ - (not self.preserve_version or self.cumulative_state == self.CUMULATIVE_STATE_YES): + if self.catalog_versions_datasets() and ( + not self.preserve_version or self.cumulative_state == self.CUMULATIVE_STATE_YES + ): - if not self.field_changed('research_dataset'): + if not self.field_changed("research_dataset"): # proceed directly to updating current record without any extra measures... return @@ -1419,12 +1656,16 @@ def _pre_update_operations(self): if actual_files_changed: - if self.preservation_state > self.PRESERVATION_STATE_INITIALIZED: # state > 0 - raise Http400({ 'detail': [ - 'Changing files is not allowed when dataset is in PAS process. Current ' - 'preservation_state = %d. In order to alter associated files, change preservation_state ' - 'back to 0.' % self.preservation_state - ]}) + if self.preservation_state > self.PRESERVATION_STATE_INITIALIZED: # state > 0 + raise Http400( + { + "detail": [ + "Changing files is not allowed when dataset is in PAS process. Current " + "preservation_state = %d. In order to alter associated files, change preservation_state " + "back to 0." % self.preservation_state + ] + } + ) elif self._files_added_for_first_time(): # first update from 0 to n files should not create a dataset version. all later updates @@ -1435,10 +1676,10 @@ def _pre_update_operations(self): self.calculate_directory_byte_sizes_and_file_counts() elif self.cumulative_state == self.CUMULATIVE_STATE_YES: - if file_changes['files_to_remove'] or file_changes['dirs_to_remove_by_project']: + if file_changes["files_to_remove"] or file_changes["dirs_to_remove_by_project"]: raise Http400( - 'Cannot delete files or directories from cumulative dataset. ' - 'In order to remove files, close dataset cumulation.' + "Cannot delete files or directories from cumulative dataset. " + "In order to remove files, close dataset cumulation." ) self._handle_cumulative_file_addition(file_changes) @@ -1456,37 +1697,50 @@ def _pre_update_operations(self): changes = self._get_metadata_file_changes() - if any((changes['files']['added'], changes['files']['removed'], - changes['directories']['added'], changes['directories']['removed'])): - - raise ValidationError({ - 'detail': [ - 'currently trying to preserve version while making changes which may result in files ' - 'being changed is not supported.' - ] - }) + if any( + ( + changes["files"]["added"], + changes["files"]["removed"], + changes["directories"]["added"], + changes["directories"]["removed"], + ) + ): + + raise ValidationError( + { + "detail": [ + "currently trying to preserve version while making changes which may result in files " + "being changed is not supported." + ] + } + ) - if self.catalog_is_harvested() and self.field_changed('research_dataset.preferred_identifier'): + if self.catalog_is_harvested() and self.field_changed( + "research_dataset.preferred_identifier" + ): self._handle_preferred_identifier_changed() def _post_update_operations(self): - if get_identifier_type(self.preferred_identifier) == IdentifierType.DOI and \ - self.update_datacite: + if ( + get_identifier_type(self.preferred_identifier) == IdentifierType.DOI + and self.update_datacite + ): self._validate_cr_against_datacite_schema() - if is_metax_generated_doi_identifier(self.research_dataset['preferred_identifier']): - self.add_post_request_callable(DataciteDOIUpdate(self, - self.research_dataset['preferred_identifier'], 'update')) + if is_metax_generated_doi_identifier(self.research_dataset["preferred_identifier"]): + self.add_post_request_callable( + DataciteDOIUpdate(self, self.research_dataset["preferred_identifier"], "update") + ) - self.add_post_request_callable(RabbitMQPublishRecord(self, 'update')) + self.add_post_request_callable(RabbitMQPublishRecord(self, "update")) log_args = { - 'event': 'dataset_updated', - 'user_id': self.user_modified or self.service_modified, - 'catalogrecord': { - 'identifier': self.identifier, - 'preferred_identifier': self.preferred_identifier, - 'data_catalog': self.data_catalog.catalog_json['identifier'], - 'date_modified': datetime_to_str(self.date_modified), + "event": "dataset_updated", + "user_id": self.user_modified or self.service_modified, + "catalogrecord": { + "identifier": self.identifier, + "preferred_identifier": self.preferred_identifier, + "data_catalog": self.data_catalog.catalog_json["identifier"], + "date_modified": datetime_to_str(self.date_modified), }, } @@ -1498,9 +1752,11 @@ def _validate_cr_against_datacite_schema(self): DataciteService, convert_cr_to_datacite_cr_json, ) + try: DataciteService().get_validated_datacite_json( - convert_cr_to_datacite_cr_json(self), True) + convert_cr_to_datacite_cr_json(self), True + ) except DataciteException as e: raise Http400(str(e)) @@ -1510,30 +1766,35 @@ def _pre_update_handle_rems(self): if self._dataset_has_rems_managed_access(): self._pre_rems_creation() else: - self._pre_rems_deletion(reason='access type change') + self._pre_rems_deletion(reason="access type change") elif self._dataset_license_changed() and self._dataset_has_rems_managed_access(): if self._dataset_has_license(): self.add_post_request_callable( - REMSUpdate(self, 'update', rems_id=self.rems_identifier, reason='license change') + REMSUpdate( + self, + "update", + rems_id=self.rems_identifier, + reason="license change", + ) ) self.rems_identifier = generate_uuid_identifier() # make sure that access_granter is not changed during license update - self.access_granter = self._initial_data['access_granter'] + self.access_granter = self._initial_data["access_granter"] else: - self._pre_rems_deletion(reason='license deletion') + self._pre_rems_deletion(reason="license deletion") - elif self.field_changed('access_granter'): + elif self.field_changed("access_granter"): # do not allow access_granter changes if no real REMS changes occur - self.access_granter = self._initial_data['access_granter'] + self.access_granter = self._initial_data["access_granter"] def _handle_cumulative_file_addition(self, file_changes): """ This method adds files to dataset only if they are explicitly mentioned in research_dataset. Changes in already included directories are not checked. """ - sql_select_and_insert_files_by_dir_path = ''' + sql_select_and_insert_files_by_dir_path = """ insert into metax_api_catalogrecord_files (catalogrecord_id, file_id) select %s as catalogrecord_id, f.id from metax_api_file as f @@ -1547,22 +1808,23 @@ def _handle_cumulative_file_addition(self, file_changes): where catalogrecord_id = %s ) returning id - ''' + """ sql_params_insert_dirs = [self.id] add_dirs_sql = [] - for project, dir_paths in file_changes['dirs_to_add_by_project'].items(): + for project, dir_paths in file_changes["dirs_to_add_by_project"].items(): for dir_path in dir_paths: - add_dirs_sql.append("(f.project_identifier = %s and f.file_path like (%s || '/%%'))") + add_dirs_sql.append( + "(f.project_identifier = %s and f.file_path like (%s || '/%%'))" + ) sql_params_insert_dirs.extend([project, dir_path]) sql_select_and_insert_files_by_dir_path = sql_select_and_insert_files_by_dir_path.replace( - 'COMPARE_PROJECT_AND_FILE_PATHS', - ' or '.join(add_dirs_sql) + "COMPARE_PROJECT_AND_FILE_PATHS", " or ".join(add_dirs_sql) ) sql_params_insert_dirs.extend([self.id]) - sql_insert_single_files = ''' + sql_insert_single_files = """ insert into metax_api_catalogrecord_files (catalogrecord_id, file_id) select %s as catalogrecord_id, f.id from metax_api_file as f @@ -1574,20 +1836,24 @@ def _handle_cumulative_file_addition(self, file_changes): where catalogrecord_id = %s ) returning id - ''' - sql_params_insert_single = [self.id, tuple(file_changes['files_to_add']), self.id] + """ + sql_params_insert_single = [ + self.id, + tuple(file_changes["files_to_add"]), + self.id, + ] with connection.cursor() as cr: n_files_added = 0 - if file_changes['files_to_add']: + if file_changes["files_to_add"]: cr.execute(sql_insert_single_files, sql_params_insert_single) n_files_added += cr.rowcount - if file_changes['dirs_to_add_by_project']: + if file_changes["dirs_to_add_by_project"]: cr.execute(sql_select_and_insert_files_by_dir_path, sql_params_insert_dirs) n_files_added += cr.rowcount - _logger.info('Added %d files to cumulative dataset %s' % (n_files_added, self.identifier)) + _logger.info("Added %d files to cumulative dataset %s" % (n_files_added, self.identifier)) self._calculate_total_files_byte_size() self.calculate_directory_byte_sizes_and_file_counts() @@ -1598,7 +1864,7 @@ def _files_added_for_first_time(self): """ Find out if this update is the first time files are being added/changed since the dataset's creation. """ - if self.files(manager='objects_unfiltered').exists(): + if self.files(manager="objects_unfiltered").exists(): # current version already has files return False @@ -1609,9 +1875,12 @@ def _files_added_for_first_time(self): return False metadata_versions_with_files_exist = ResearchDatasetVersion.objects.filter( - Q(Q(research_dataset__files__isnull=False) | Q(research_dataset__directories__isnull=False)), - catalog_record_id=self.id) \ - .exists() + Q( + Q(research_dataset__files__isnull=False) + | Q(research_dataset__directories__isnull=False) + ), + catalog_record_id=self.id, + ).exists() # metadata_versions_with_files_exist == True implies this "0 to n" update without # creating a new dataset version already occurred once @@ -1626,7 +1895,7 @@ def _pre_rems_creation(self): user_info = self._get_user_info_for_rems() self.access_granter = user_info self.rems_identifier = generate_uuid_identifier() - self.add_post_request_callable(REMSUpdate(self, 'create', user_info=user_info)) + self.add_post_request_callable(REMSUpdate(self, "create", user_info=user_info)) def _pre_rems_deletion(self, reason): """ @@ -1634,7 +1903,7 @@ def _pre_rems_deletion(self, reason): corresponding REMS entity. """ self.add_post_request_callable( - REMSUpdate(self, 'close', rems_id=self.rems_identifier, reason=reason) + REMSUpdate(self, "close", rems_id=self.rems_identifier, reason=reason) ) self.rems_identifier = None self.access_granter = None @@ -1655,7 +1924,9 @@ def _dataset_rems_changed(self): """ Check if dataset is updated so that REMS needs to be updated. """ - return self.catalog_is_ida() and (self._dataset_rems_access_type_changed() or self._dataset_license_changed()) + return self.catalog_is_ida() and ( + self._dataset_rems_access_type_changed() or self._dataset_license_changed() + ) def _dataset_has_license(self): """ @@ -1671,24 +1942,27 @@ def _dataset_license_changed(self): """ from metax_api.services import CatalogRecordService as CRS - return CRS.get_research_dataset_license_url(self.research_dataset) != \ - CRS.get_research_dataset_license_url(self._initial_data['research_dataset']) + return CRS.get_research_dataset_license_url( + self.research_dataset + ) != CRS.get_research_dataset_license_url(self._initial_data["research_dataset"]) def _calculate_total_files_byte_size(self): rd = self.research_dataset - if 'files' in rd or 'directories' in rd: - rd['total_files_byte_size'] = self.files.aggregate(Sum('byte_size'))['byte_size__sum'] or 0 + if "files" in rd or "directories" in rd: + rd["total_files_byte_size"] = ( + self.files.aggregate(Sum("byte_size"))["byte_size__sum"] or 0 + ) else: - rd['total_files_byte_size'] = 0 + rd["total_files_byte_size"] = 0 def _calculate_total_remote_resources_byte_size(self): rd = self.research_dataset - if 'remote_resources' in rd: - rd['total_remote_resources_byte_size'] = sum( - rr['byte_size'] for rr in rd['remote_resources'] if 'byte_size' in rr + if "remote_resources" in rd: + rd["total_remote_resources_byte_size"] = sum( + rr["byte_size"] for rr in rd["remote_resources"] if "byte_size" in rr ) else: - rd['total_remote_resources_byte_size'] = 0 + rd["total_remote_resources_byte_size"] = 0 def _get_dataset_selected_file_ids(self): """ @@ -1696,14 +1970,19 @@ def _get_dataset_selected_file_ids(self): of all unique individual files currently in the db. """ file_ids = [] - file_changes = { 'changed_projects': defaultdict(set) } + file_changes = {"changed_projects": defaultdict(set)} - if 'files' in self.research_dataset: - file_ids.extend(self._get_file_ids_from_file_list(self.research_dataset['files'], file_changes)) + if "files" in self.research_dataset: + file_ids.extend( + self._get_file_ids_from_file_list(self.research_dataset["files"], file_changes) + ) - if 'directories' in self.research_dataset: - file_ids.extend(self._get_file_ids_from_dir_list(self.research_dataset['directories'], - file_ids, file_changes)) + if "directories" in self.research_dataset: + file_ids.extend( + self._get_file_ids_from_dir_list( + self.research_dataset["directories"], file_ids, file_changes + ) + ) self._check_changed_files_permissions(file_changes) @@ -1719,20 +1998,28 @@ def _get_file_ids_from_file_list(self, file_list, file_changes): if not file_list: return [] - file_pids = [ f['identifier'] for f in file_list ] + file_pids = [f["identifier"] for f in file_list] - files = File.objects.filter(identifier__in=file_pids).values('id', 'identifier', 'project_identifier') + files = File.objects.filter(identifier__in=file_pids).values( + "id", "identifier", "project_identifier" + ) file_ids = [] for f in files: - file_ids.append(f['id']) - file_changes['changed_projects']['files_added'].add(f['project_identifier']) + file_ids.append(f["id"]) + file_changes["changed_projects"]["files_added"].add(f["project_identifier"]) if len(file_ids) != len(file_pids): - missing_identifiers = [ pid for pid in file_pids if pid not in set(f['identifier'] for f in files)] - raise ValidationError({ 'detail': [ - 'some requested files were not found. file identifiers not found:\n%s' - % '\n'.join(missing_identifiers) - ]}) + missing_identifiers = [ + pid for pid in file_pids if pid not in set(f["identifier"] for f in files) + ] + raise ValidationError( + { + "detail": [ + "some requested files were not found. file identifiers not found:\n%s" + % "\n".join(missing_identifiers) + ] + } + ) return file_ids @@ -1748,7 +2035,7 @@ def _get_file_ids_from_dir_list(self, dirs_list, ignore_files, file_changes): if not dirs_list: return [] - dir_identifiers = [ d['identifier'] for d in dirs_list ] + dir_identifiers = [d["identifier"] for d in dirs_list] highest_level_dirs_by_project = self._get_top_level_parent_dirs_by_project(dir_identifiers) @@ -1759,11 +2046,11 @@ def _get_file_ids_from_dir_list(self, dirs_list, ignore_files, file_changes): for dir_path in dir_paths: files = files | File.objects.filter( project_identifier=project_identifier, - file_path__startswith='%s/' % dir_path + file_path__startswith="%s/" % dir_path, ) - file_changes['changed_projects']['files_added'].add(project_identifier) + file_changes["changed_projects"]["files_added"].add(project_identifier) - return files.exclude(id__in=ignore_files).values_list('id', flat=True) + return files.exclude(id__in=ignore_files).values_list("id", flat=True) def _get_top_level_parent_dirs_by_project(self, dir_identifiers): """ @@ -1776,23 +2063,31 @@ def _get_top_level_parent_dirs_by_project(self, dir_identifiers): if not dir_identifiers: return {} - dirs = Directory.objects.filter(identifier__in=dir_identifiers) \ - .values('project_identifier', 'directory_path', 'identifier') \ - .order_by('project_identifier', 'directory_path') + dirs = ( + Directory.objects.filter(identifier__in=dir_identifiers) + .values("project_identifier", "directory_path", "identifier") + .order_by("project_identifier", "directory_path") + ) # skip deprecated datasets, since there might be deleted directories if len(dirs) != len(dir_identifiers) and not self.deprecated: - missing_identifiers = [ pid for pid in dir_identifiers if pid not in set(d['identifier'] for d in dirs)] - raise ValidationError({ 'detail': [ - 'some requested directories were not found. directory identifiers not found:\n%s' - % '\n'.join(missing_identifiers) - ]}) + missing_identifiers = [ + pid for pid in dir_identifiers if pid not in set(d["identifier"] for d in dirs) + ] + raise ValidationError( + { + "detail": [ + "some requested directories were not found. directory identifiers not found:\n%s" + % "\n".join(missing_identifiers) + ] + } + ) # group directory paths by project dirs_by_project = defaultdict(list) for dr in dirs: - dirs_by_project[dr['project_identifier']].append(dr['directory_path']) + dirs_by_project[dr["project_identifier"]].append(dr["directory_path"]) top_level_dirs_by_project = defaultdict(list) @@ -1800,14 +2095,16 @@ def _get_top_level_parent_dirs_by_project(self, dir_identifiers): for path in dir_paths: - dir_is_root = [ p.startswith('%s/' % path) for p in dir_paths if p != path ] + dir_is_root = [p.startswith("%s/" % path) for p in dir_paths if p != path] if all(dir_is_root): # found the root dir. disregard all the rest of the paths, if there were any. top_level_dirs_by_project[proj] = [path] break else: - path_contained_by_other_paths = [ path.startswith('%s/' % p) for p in dir_paths if p != path ] + path_contained_by_other_paths = [ + path.startswith("%s/" % p) for p in dir_paths if p != path + ] if any(path_contained_by_other_paths): # a child of at least one other path. no need to include it in the list @@ -1821,7 +2118,7 @@ def _get_top_level_parent_dirs_by_project(self, dir_identifiers): def _files_changed(self): file_changes = self._find_file_changes() - if not file_changes['changed_projects']: + if not file_changes["changed_projects"]: # no changes in directory or file entries were detected. return False, None elif self._files_added_for_first_time(): @@ -1853,20 +2150,24 @@ def _create_temp_record(self, file_changes): temp_record = self._create_new_dataset_version_template() - actual_files_changed = self._process_file_changes(file_changes, temp_record.id, self.id) + actual_files_changed = self._process_file_changes( + file_changes, temp_record.id, self.id + ) if actual_files_changed: if self.cumulative_state == self.CUMULATIVE_STATE_YES: - _logger.info('Files changed, but dataset is cumulative') + _logger.info("Files changed, but dataset is cumulative") raise DiscardRecord() elif self.catalog_is_pas(): - raise Http400('Cannot change files in a dataset in PAS catalog.') + raise Http400("Cannot change files in a dataset in PAS catalog.") self._new_version = temp_record else: - _logger.debug('no real file changes detected, discarding the temporary record...') + _logger.debug( + "no real file changes detected, discarding the temporary record..." + ) raise DiscardRecord() except DiscardRecord: # rolled back @@ -1889,58 +2190,68 @@ def _create_new_dataset_version_template(self): new_version_template.previous_dataset_version = None new_version_template.dataset_version_set = None new_version_template.identifier = generate_uuid_identifier() - new_version_template.research_dataset['metadata_version_identifier'] = generate_uuid_identifier() + new_version_template.research_dataset[ + "metadata_version_identifier" + ] = generate_uuid_identifier() new_version_template.preservation_identifier = None - new_version_template.api_meta['version'] = self.api_version + new_version_template.api_meta["version"] = self.api_version super(Common, new_version_template).save() return new_version_template def _check_changed_files_permissions(self, file_changes): - ''' + """ Ensure user belongs to projects of all changed files and dirs. Raises 403 on error. - ''' - if not self.request: # pragma: no cover + """ + if not self.request: # pragma: no cover # when files associated with a dataset have been changed, the user should be # always known, i.e. the http request object is present. if its not, the code # is not being executed as a result of a api request. in that case, only allow # proceeding when the code is executed for testing: the code is being called directly # from a test case, to set up test conditions etc. - assert executing_test_case(), 'only permitted when setting up testing conditions' + assert executing_test_case(), "only permitted when setting up testing conditions" return - projects_added = file_changes['changed_projects'].get('files_added', set()) - projects_removed = file_changes['changed_projects'].get('files_removed', set()) + projects_added = file_changes["changed_projects"].get("files_added", set()) + projects_removed = file_changes["changed_projects"].get("files_removed", set()) project_changes = projects_added.union(projects_removed) from metax_api.services import CommonService - allowed_projects = CommonService.get_list_query_param(self.request, 'allowed_projects') + + allowed_projects = CommonService.get_list_query_param(self.request, "allowed_projects") if allowed_projects is not None: if not all(p in allowed_projects for p in project_changes): - raise Http403({'detail': [ - 'Unable to update dataset %s. You do not have permissions to all of the files and directories.' - % self.identifier - ]}) + raise Http403( + { + "detail": [ + "Unable to update dataset %s. You do not have permissions to all of the files and directories." + % self.identifier + ] + } + ) if self.request.user.is_service: # assumed the service knows what it is doing return from metax_api.services import AuthService + user_projects = AuthService.get_user_projects(self.request) - invalid_project_perms = [ proj for proj in project_changes if proj not in user_projects ] + invalid_project_perms = [proj for proj in project_changes if proj not in user_projects] if invalid_project_perms: - raise Http403({ - 'detail': [ - 'Unable to add files to dataset. You are lacking project membership in the following projects: %s' - % ', '.join(invalid_project_perms) - ] - }) + raise Http403( + { + "detail": [ + "Unable to add files to dataset. You are lacking project membership in the following projects: %s" + % ", ".join(invalid_project_perms) + ] + } + ) def _handle_metadata_versioning(self): if not self.research_dataset_versions.exists(): @@ -1948,20 +2259,22 @@ def _handle_metadata_versioning(self): # when the first new version is created, first add the initial version. first_rdv = ResearchDatasetVersion( date_created=self.date_created, - metadata_version_identifier=self._initial_data['research_dataset']['metadata_version_identifier'], + metadata_version_identifier=self._initial_data["research_dataset"][ + "metadata_version_identifier" + ], preferred_identifier=self.preferred_identifier, - research_dataset=self._initial_data['research_dataset'], + research_dataset=self._initial_data["research_dataset"], catalog_record=self, ) first_rdv.save() # create and add the new metadata version - self.research_dataset['metadata_version_identifier'] = generate_uuid_identifier() + self.research_dataset["metadata_version_identifier"] = generate_uuid_identifier() new_rdv = ResearchDatasetVersion( date_created=self.date_modified, - metadata_version_identifier=self.research_dataset['metadata_version_identifier'], + metadata_version_identifier=self.research_dataset["metadata_version_identifier"], preferred_identifier=self.preferred_identifier, research_dataset=self.research_dataset, catalog_record=self, @@ -1972,18 +2285,22 @@ def _create_new_dataset_version(self): """ Create a new dataset version of the record who calls this method. """ - assert hasattr(self, '_new_version'), 'self._new_version should have been set in a previous step' + assert hasattr( + self, "_new_version" + ), "self._new_version should have been set in a previous step" old_version = self if old_version.next_dataset_version_id: - raise ValidationError({ 'detail': ['Changing files in old dataset versions is not permitted.'] }) + raise ValidationError( + {"detail": ["Changing files in old dataset versions is not permitted."]} + ) _logger.info( - 'Files changed during CatalogRecord update. Creating new dataset version ' - 'from old CatalogRecord having identifier %s' % old_version.identifier + "Files changed during CatalogRecord update. Creating new dataset version " + "from old CatalogRecord having identifier %s" % old_version.identifier ) _logger.debug( - 'Old CR metadata version identifier: %s' % old_version.metadata_version_identifier + "Old CR metadata version identifier: %s" % old_version.metadata_version_identifier ) new_version = self._new_version @@ -2010,61 +2327,59 @@ def _create_new_dataset_version(self): # contains the new field data from the request. this effectively transfers # the changes to the new dataset version. new_version.research_dataset = deepcopy(old_version.research_dataset) - new_version.research_dataset['metadata_version_identifier'] = generate_uuid_identifier() + new_version.research_dataset["metadata_version_identifier"] = generate_uuid_identifier() # This effectively means one cannot change identifier type for new catalog record versions - pref_id_type = get_identifier_type(old_version.research_dataset['preferred_identifier']) + pref_id_type = get_identifier_type(old_version.research_dataset["preferred_identifier"]) if pref_id_type == IdentifierType.URN: - new_version.research_dataset['preferred_identifier'] = generate_uuid_identifier(urn_prefix=True) + new_version.research_dataset["preferred_identifier"] = generate_uuid_identifier( + urn_prefix=True + ) elif pref_id_type == IdentifierType.DOI: doi_id = generate_doi_identifier() - new_version.research_dataset['preferred_identifier'] = doi_id + new_version.research_dataset["preferred_identifier"] = doi_id if self.catalog_is_ida(): new_version.preservation_identifier = doi_id else: - _logger.debug("This code should never be reached. Using URN identifier for the new version pref id") - self.research_dataset['preferred_identifier'] = generate_uuid_identifier(urn_prefix=True) + _logger.debug( + "This code should never be reached. Using URN identifier for the new version pref id" + ) + self.research_dataset["preferred_identifier"] = generate_uuid_identifier( + urn_prefix=True + ) new_version._calculate_total_files_byte_size() - if 'remote_resources' in new_version.research_dataset: + if "remote_resources" in new_version.research_dataset: new_version._calculate_total_remote_resources_byte_size() # nothing must change in the now old version of research_dataset, so copy # from _initial_data so that super().save() does not change it later. - old_version.research_dataset = deepcopy(old_version._initial_data['research_dataset']) + old_version.research_dataset = deepcopy(old_version._initial_data["research_dataset"]) old_version.next_dataset_version = new_version - if new_version.editor: - # some of the old editor fields cant be true in the new version, so keep - # only the ones that make sense. it is up to the editor, to update other fields - # they see as relevant. we also dont want null values in there - old_editor = deepcopy(new_version.editor) - new_version.editor = {} - if 'owner_id' in old_editor: - new_version.editor['owner_id'] = old_editor['owner_id'] - if 'creator_id' in old_editor: - new_version.editor['creator_id'] = old_editor['creator_id'] - if 'identifier' in old_editor: - # todo this probably does not make sense... ? - new_version.editor['identifier'] = old_editor['identifier'] - super(Common, new_version).save() new_version.calculate_directory_byte_sizes_and_file_counts() - if is_metax_generated_doi_identifier(self.research_dataset['preferred_identifier']): - self.add_post_request_callable(DataciteDOIUpdate(new_version, - new_version.research_dataset['preferred_identifier'], - 'create')) + if is_metax_generated_doi_identifier(self.research_dataset["preferred_identifier"]): + self.add_post_request_callable( + DataciteDOIUpdate( + new_version, + new_version.research_dataset["preferred_identifier"], + "create", + ) + ) - new_version.add_post_request_callable(RabbitMQPublishRecord(new_version, 'create')) + new_version.add_post_request_callable(RabbitMQPublishRecord(new_version, "create")) old_version.new_dataset_version_created = new_version.identifiers_dict - old_version.new_dataset_version_created['version_type'] = 'dataset' + old_version.new_dataset_version_created["version_type"] = "dataset" - _logger.info('New dataset version created, identifier %s' % new_version.identifier) - _logger.debug('New dataset version preferred identifer %s' % new_version.preferred_identifier) + _logger.info("New dataset version created, identifier %s" % new_version.identifier) + _logger.debug( + "New dataset version preferred identifer %s" % new_version.preferred_identifier + ) def _get_metadata_file_changes(self): """ @@ -2074,28 +2389,32 @@ def _get_metadata_file_changes(self): Note: set removes duplicates. It is assumed that file listings do not include duplicate files. """ - if not self._field_is_loaded('research_dataset'): + if not self._field_is_loaded("research_dataset"): return {} - if not self._field_initial_value_loaded('research_dataset'): # pragma: no cover - self._raise_field_not_tracked_error('research_dataset.files') + if not self._field_initial_value_loaded("research_dataset"): # pragma: no cover + self._raise_field_not_tracked_error("research_dataset.files") changes = {} - initial_files = set( f['identifier'] for f in self._initial_data['research_dataset'].get('files', []) ) - received_files = set( f['identifier'] for f in self.research_dataset.get('files', []) ) - changes['files'] = { - 'keep': initial_files.intersection(received_files), - 'removed': initial_files.difference(received_files), - 'added': received_files.difference(initial_files), + initial_files = set( + f["identifier"] for f in self._initial_data["research_dataset"].get("files", []) + ) + received_files = set(f["identifier"] for f in self.research_dataset.get("files", [])) + changes["files"] = { + "keep": initial_files.intersection(received_files), + "removed": initial_files.difference(received_files), + "added": received_files.difference(initial_files), } - initial_dirs = set(dr['identifier'] for dr in self._initial_data['research_dataset'].get('directories', [])) - received_dirs = set( dr['identifier'] for dr in self.research_dataset.get('directories', []) ) - changes['directories'] = { - 'keep': initial_dirs.intersection(received_dirs), - 'removed': initial_dirs.difference(received_dirs), - 'added': received_dirs.difference(initial_dirs), + initial_dirs = set( + dr["identifier"] for dr in self._initial_data["research_dataset"].get("directories", []) + ) + received_dirs = set(dr["identifier"] for dr in self.research_dataset.get("directories", [])) + changes["directories"] = { + "keep": initial_dirs.intersection(received_dirs), + "removed": initial_dirs.difference(received_dirs), + "added": received_dirs.difference(initial_dirs), } return changes @@ -2104,18 +2423,20 @@ def calculate_directory_byte_sizes_and_file_counts(self): """ Calculate directory byte_sizes and file_counts for all dirs selected for this cr. """ - _logger.info('Calculating directory byte_sizes and file_counts...') + _logger.info("Calculating directory byte_sizes and file_counts...") dir_identifiers = file_dir_identifiers = [] - if self.research_dataset.get('directories', None): - dir_identifiers = [d['identifier'] for d in self.research_dataset['directories']] + if self.research_dataset.get("directories", None): + dir_identifiers = [d["identifier"] for d in self.research_dataset["directories"]] file_dir_identifiers = [] - if self.research_dataset.get('files', None): + if self.research_dataset.get("files", None): try: - file_dir_identifiers = [File.objects.get(identifier=f['identifier']).parent_directory.identifier - for f in self.research_dataset['files']] + file_dir_identifiers = [ + File.objects.get(identifier=f["identifier"]).parent_directory.identifier + for f in self.research_dataset["files"] + ] except Exception as e: _logger.error(e) @@ -2132,7 +2453,9 @@ def calculate_directory_byte_sizes_and_file_counts(self): directory_data = {} for project_identifier, dir_paths in highest_level_dirs_by_project.items(): - dirs = Directory.objects.filter(project_identifier=project_identifier, directory_path__in=dir_paths) + dirs = Directory.objects.filter( + project_identifier=project_identifier, directory_path__in=dir_paths + ) for dr in dirs: dr.calculate_byte_size_and_file_count_for_cr(self.id, directory_data) @@ -2140,11 +2463,12 @@ def calculate_directory_byte_sizes_and_file_counts(self): self.get_dirs_by_parent(dirs, directory_data) self._directory_data = directory_data - super(Common, self).save(update_fields=['_directory_data']) + super(Common, self).save(update_fields=["_directory_data"]) def get_dirs_by_parent(self, ids, directory_data): - dirs = Directory.objects.filter(id__in=ids, parent_directory_id__isnull=False) \ - .values_list('id', 'parent_directory_id') + dirs = Directory.objects.filter(id__in=ids, parent_directory_id__isnull=False).values_list( + "id", "parent_directory_id" + ) ids = [] if dirs: @@ -2177,17 +2501,20 @@ def _handle_preservation_state_changed(self): """ self.preservation_state_modified = self.date_modified - old_value = self._initial_data['preservation_state'] + old_value = self._initial_data["preservation_state"] new_value = self.preservation_state - _logger.info('preservation_state changed from %d to %d' % (old_value, new_value)) + _logger.info("preservation_state changed from %d to %d" % (old_value, new_value)) - if not self.preservation_dataset_origin_version_exists() and self.preservation_dataset_version is None \ - and self.catalog_is_pas(): + if ( + not self.preservation_dataset_origin_version_exists() + and self.preservation_dataset_version is None + and self.catalog_is_pas() + ): # dataset was created directly into PAS catalog. do nothing, no rules # are enforced (for now) _logger.info( - 'preservation_state changed from %d to %d. (native PAS catalog dataset)' + "preservation_state changed from %d to %d. (native PAS catalog dataset)" % (old_value, new_value) ) return @@ -2196,109 +2523,133 @@ def _handle_preservation_state_changed(self): if new_value == 0 and self.catalog_is_pas(): - _logger.info('Tried to set preservation_state to 0 on a PAS dataset. Aborting') + _logger.info("Tried to set preservation_state to 0 on a PAS dataset. Aborting") - raise Http400({ 'detail': [ - 'Can\'t set preservation_state to 0 on a PAS version. Set to %d or %d in order to conclude PAS process.' - % (self.PRESERVATION_STATE_IN_PAS, self.PRESERVATION_STATE_REJECTED_FROM_PAS) - ]}) + raise Http400( + { + "detail": [ + "Can't set preservation_state to 0 on a PAS version. Set to %d or %d in order to conclude PAS process." + % ( + self.PRESERVATION_STATE_IN_PAS, + self.PRESERVATION_STATE_REJECTED_FROM_PAS, + ) + ] + } + ) elif new_value <= self.PRESERVATION_STATE_ACCEPTED_TO_PAS and self.catalog_is_pas(): - raise Http400({ 'detail': [ - 'preservation_state values in PAS catalog should be over 80 (accepted to PAS)' - ]}) + raise Http400( + { + "detail": [ + "preservation_state values in PAS catalog should be over 80 (accepted to PAS)" + ] + } + ) elif new_value > self.PRESERVATION_STATE_ACCEPTED_TO_PAS and not self.catalog_is_pas(): - raise Http400({ 'detail': [ - 'Maximum value of preservation_state in a non-PAS catalog is 80 (accepted to PAS)' - ]}) + raise Http400( + { + "detail": [ + "Maximum value of preservation_state in a non-PAS catalog is 80 (accepted to PAS)" + ] + } + ) elif new_value == self.PRESERVATION_STATE_ACCEPTED_TO_PAS: if self.catalog_is_pas(): - raise Http400({ 'detail': [ - 'Dataset is already in PAS catalog' - ]}) + raise Http400({"detail": ["Dataset is already in PAS catalog"]}) elif self.preservation_dataset_version: - raise Http400({ 'detail': [ - 'Dataset already has a PAS version. Identifier: %s' % self.preservation_dataset_version.identifier - ]}) + raise Http400( + { + "detail": [ + "Dataset already has a PAS version. Identifier: %s" + % self.preservation_dataset_version.identifier + ] + } + ) else: self._create_pas_version(self) - _logger.info('Resetting preservation_state of original dataset to 0') + _logger.info("Resetting preservation_state of original dataset to 0") self.preservation_state = self.PRESERVATION_STATE_INITIALIZED self.preservation_description = None self.preservation_reason_description = None - elif new_value in (self.PRESERVATION_STATE_IN_PAS, self.PRESERVATION_STATE_REJECTED_FROM_PAS): - _logger.info('PAS-process concluded') + elif new_value in ( + self.PRESERVATION_STATE_IN_PAS, + self.PRESERVATION_STATE_REJECTED_FROM_PAS, + ): + _logger.info("PAS-process concluded") else: - _logger.debug('preservation_state change not handled for these values') + _logger.debug("preservation_state change not handled for these values") def _create_pas_version(self, origin_version): """ Create PAS version to PAS catalog and add related links. """ - _logger.info('Creating new PAS dataset version...') + _logger.info("Creating new PAS dataset version...") if origin_version.preservation_dataset_version_id: - msg = 'Dataset already has a PAS version. Identifier of PAS dataset: %s' \ + msg = ( + "Dataset already has a PAS version. Identifier of PAS dataset: %s" % origin_version.preservation_dataset_version.identifier + ) _logger.info(msg) - raise Http400({ 'detail': [msg] }) + raise Http400({"detail": [msg]}) try: - pas_catalog = DataCatalog.objects.only('id').get( + pas_catalog = DataCatalog.objects.only("id").get( catalog_json__identifier=settings.PAS_DATA_CATALOG_IDENTIFIER ) except DataCatalog.DoesNotExist: - msg = 'PAS catalog %s does not exist' % settings.PAS_DATA_CATALOG_IDENTIFIER + msg = "PAS catalog %s does not exist" % settings.PAS_DATA_CATALOG_IDENTIFIER _logger.info(msg) - raise Http400({ 'detail': [msg] }) + raise Http400({"detail": [msg]}) research_dataset = deepcopy(origin_version.research_dataset) - research_dataset.pop('preferred_identifier', None) - research_dataset.pop('metadata_version_identifier', None) + research_dataset.pop("preferred_identifier", None) + research_dataset.pop("metadata_version_identifier", None) params = { - 'data_catalog': pas_catalog, - 'research_dataset': research_dataset, - 'contract': origin_version.contract, - 'date_created': origin_version.date_modified, - 'service_created': origin_version.service_modified, - 'user_created': origin_version.user_modified, - 'metadata_owner_org': origin_version.metadata_owner_org, - 'metadata_provider_org': origin_version.metadata_provider_org, - 'metadata_provider_user': origin_version.metadata_provider_user, - 'preservation_state': origin_version.preservation_state, - 'preservation_description': origin_version.preservation_description, - 'preservation_reason_description': origin_version.preservation_reason_description, - 'preservation_dataset_origin_version': origin_version, + "data_catalog": pas_catalog, + "research_dataset": research_dataset, + "contract": origin_version.contract, + "date_created": origin_version.date_modified, + "service_created": origin_version.service_modified, + "user_created": origin_version.user_modified, + "metadata_owner_org": origin_version.metadata_owner_org, + "metadata_provider_org": origin_version.metadata_provider_org, + "metadata_provider_user": origin_version.metadata_provider_user, + "preservation_state": origin_version.preservation_state, + "preservation_description": origin_version.preservation_description, + "preservation_reason_description": origin_version.preservation_reason_description, + "preservation_dataset_origin_version": origin_version, } # add information about other identifiers for this dataset other_identifiers_info_origin = { - 'notation': origin_version.preferred_identifier, - 'type': { - 'identifier': get_identifier_type(origin_version.preferred_identifier).value, - } + "notation": origin_version.preferred_identifier, + "type": { + "identifier": get_identifier_type(origin_version.preferred_identifier).value, + }, } try: - params['research_dataset']['other_identifier'].append(other_identifiers_info_origin) + params["research_dataset"]["other_identifier"].append(other_identifiers_info_origin) except KeyError: - params['research_dataset']['other_identifier'] = [other_identifiers_info_origin] + params["research_dataset"]["other_identifier"] = [other_identifiers_info_origin] # validate/populate fields according to reference data from metax_api.services import CatalogRecordService as CRS, RedisCacheService as cache - CRS.validate_reference_data(params['research_dataset'], cache) + + CRS.validate_reference_data(params["research_dataset"], cache) # finally create the pas copy dataset pas_version = self.__class__(**params) @@ -2308,32 +2659,32 @@ def _create_pas_version(self, origin_version): # ensure pas dataset contains exactly the same files as origin dataset. clear the result # that was achieved by calling save(), which processed research_dataset.files and research_dataset.directories pas_version.files.clear() - pas_version.files.add(*origin_version.files.filter().values_list('id', flat=True)) + pas_version.files.add(*origin_version.files.filter().values_list("id", flat=True)) # link origin_version and pas copy origin_version.preservation_dataset_version = pas_version origin_version.new_dataset_version_created = pas_version.identifiers_dict - origin_version.new_dataset_version_created['version_type'] = 'pas' + origin_version.new_dataset_version_created["version_type"] = "pas" # add information about other identifiers for origin_dataset other_identifiers_info_pas = { - 'notation': pas_version.preferred_identifier, - 'type': { - 'identifier': get_identifier_type(pas_version.preferred_identifier).value, - } + "notation": pas_version.preferred_identifier, + "type": { + "identifier": get_identifier_type(pas_version.preferred_identifier).value, + }, } try: - origin_version.research_dataset['other_identifier'].append(other_identifiers_info_pas) + origin_version.research_dataset["other_identifier"].append(other_identifiers_info_pas) except KeyError: - origin_version.research_dataset['other_identifier'] = [other_identifiers_info_pas] + origin_version.research_dataset["other_identifier"] = [other_identifiers_info_pas] # need to validate ref data again for origin_version CRS.validate_reference_data(origin_version.research_dataset, cache) - self.add_post_request_callable(RabbitMQPublishRecord(pas_version, 'create')) + self.add_post_request_callable(RabbitMQPublishRecord(pas_version, "create")) - _logger.info('PAS dataset version created with identifier: %s' % pas_version.identifier) + _logger.info("PAS dataset version created with identifier: %s" % pas_version.identifier) def _check_alternate_records(self): """ @@ -2350,10 +2701,12 @@ def _check_alternate_records(self): below makes only one query. We are only interested in alternate_record_set though, since fetching it now saves another query later when checking if it already exists. """ - return CatalogRecord.objects.select_related('data_catalog', 'alternate_record_set') \ - .filter(research_dataset__contains={ 'preferred_identifier': self.preferred_identifier }) \ - .exclude(Q(data_catalog__id=self.data_catalog_id) | Q(id=self.id)) \ + return ( + CatalogRecord.objects.select_related("data_catalog", "alternate_record_set") + .filter(research_dataset__contains={"preferred_identifier": self.preferred_identifier}) + .exclude(Q(data_catalog__id=self.data_catalog_id) | Q(id=self.id)) .first() + ) def _create_or_update_alternate_record_set(self, other_record): """ @@ -2370,8 +2723,14 @@ def _create_or_update_alternate_record_set(self, other_record): ars = AlternateRecordSet() ars.save() ars.records.add(self, other_record) - _logger.info('Creating new alternate_record_set for preferred_identifier: %s, with records: %s and %s' % - (self.preferred_identifier, self.metadata_version_identifier, other_record.metadata_version_identifier)) + _logger.info( + "Creating new alternate_record_set for preferred_identifier: %s, with records: %s and %s" + % ( + self.preferred_identifier, + self.metadata_version_identifier, + other_record.metadata_version_identifier, + ) + ) def _remove_from_alternate_record_set(self): """ @@ -2393,24 +2752,29 @@ def add_post_request_callable(self, callable): In case of drafts, skip other than logging """ if not self.is_published() and not isinstance(callable, DelayedLog): - _logger.debug(f'{self.identifier} is a draft, skipping non-logging post request callables') + _logger.debug( + f"{self.identifier} is a draft, skipping non-logging post request callables" + ) return from metax_api.services import CallableService + CallableService.add_post_request_callable(callable) def __repr__(self): - return '<%s: %d, removed: %s, data_catalog: %s, metadata_version_identifier: %s, ' \ - 'preferred_identifier: %s, file_count: %d >' \ + return ( + "<%s: %d, removed: %s, data_catalog: %s, metadata_version_identifier: %s, " + "preferred_identifier: %s, file_count: %d >" % ( - 'CatalogRecord', + "CatalogRecord", self.id, str(self.removed), - self.data_catalog.catalog_json['identifier'], + self.data_catalog.catalog_json["identifier"], self.metadata_version_identifier, self.preferred_identifier, self.files.count(), ) + ) def _get_preferred_identifier_type_from_request(self): """ @@ -2419,7 +2783,7 @@ def _get_preferred_identifier_type_from_request(self): :return: IdentifierType. Return None if parameter not given or is unrecognized. Calling code is then responsible for choosing which IdentifierType to use. """ - pid_type = self.request.query_params.get('pid_type', None) + pid_type = self.request.query_params.get("pid_type", None) if pid_type == IdentifierType.DOI.value: pid_type = IdentifierType.DOI elif pid_type == IdentifierType.URN.value: @@ -2435,22 +2799,23 @@ def change_cumulative_state(self, new_state): self._assert_api_version() if self.next_dataset_version: - raise Http400('Cannot change cumulative_state on old dataset version') + raise Http400("Cannot change cumulative_state on old dataset version") - cumulative_state_valid_values = [ choice[0] for choice in self.CUMULATIVE_STATE_CHOICES ] + cumulative_state_valid_values = [choice[0] for choice in self.CUMULATIVE_STATE_CHOICES] try: new_state = int(new_state) assert new_state in cumulative_state_valid_values except: raise Http400( - 'cumulative_state must be one of: %s' % ', '.join(str(x) for x in cumulative_state_valid_values) + "cumulative_state must be one of: %s" + % ", ".join(str(x) for x in cumulative_state_valid_values) ) - _logger.info('Changing cumulative_state from %d to %d' % (self.cumulative_state, new_state)) + _logger.info("Changing cumulative_state from %d to %d" % (self.cumulative_state, new_state)) if self.cumulative_state == new_state: - _logger.info('No change in cumulative_state') + _logger.info("No change in cumulative_state") return False self.date_modified = get_tz_aware_now_without_micros() @@ -2462,31 +2827,33 @@ def change_cumulative_state(self, new_state): if new_state == self.CUMULATIVE_STATE_NO: raise Http400( - 'Cumulative dataset cannot be set to non-cumulative dataset. ' - 'If you want to stop active cumulation, set cumulative status to closed.' + "Cumulative dataset cannot be set to non-cumulative dataset. " + "If you want to stop active cumulation, set cumulative status to closed." ) elif new_state == self.CUMULATIVE_STATE_CLOSED: if self.cumulative_state == self.CUMULATIVE_STATE_NO: - raise Http400('Cumulation cannot be closed for non-cumulative dataset') + raise Http400("Cumulation cannot be closed for non-cumulative dataset") self.date_cumulation_ended = self.date_modified self.cumulative_state = new_state - super().save(update_fields=[ - 'cumulative_state', - 'date_cumulation_ended', - 'date_modified' - ]) + super().save( + update_fields=[ + "cumulative_state", + "date_cumulation_ended", + "date_modified", + ] + ) elif new_state == self.CUMULATIVE_STATE_YES: if self.preservation_state > self.PRESERVATION_STATE_INITIALIZED: raise Http400( - 'Cumulative datasets are not allowed in PAS process. Change preservation_state ' - 'to 0 in order to change the dataset to cumulative.' + "Cumulative datasets are not allowed in PAS process. Change preservation_state " + "to 0 in order to change the dataset to cumulative." ) new_version = self._create_new_dataset_version_template() @@ -2496,7 +2863,7 @@ def change_cumulative_state(self, new_state): super(Common, new_version).save() # add all files from previous version to new version - new_version.files.add(*self.files.values_list('id', flat=True)) + new_version.files.add(*self.files.values_list("id", flat=True)) self._new_version = new_version @@ -2504,7 +2871,7 @@ def change_cumulative_state(self, new_state): super().save() - self.add_post_request_callable(RabbitMQPublishRecord(self, 'update')) + self.add_post_request_callable(RabbitMQPublishRecord(self, "update")) return True if new_state == self.CUMULATIVE_STATE_YES else False @@ -2518,27 +2885,32 @@ def refresh_directory_content(self, dir_identifier): self._assert_api_version() if self.deprecated: - raise Http400('Cannot update files on deprecated dataset. ' - 'You can remove all deleted files from dataset using API /rpc/datasets/fix_deprecated.') + raise Http400( + "Cannot update files on deprecated dataset. " + "You can remove all deleted files from dataset using API /rpc/datasets/fix_deprecated." + ) try: dir = Directory.objects.get(identifier=dir_identifier) except Directory.DoesNotExist: - raise Http404(f'Directory \'{dir_identifier}\' could not be found') + raise Http404(f"Directory '{dir_identifier}' could not be found") - dir_identifiers = [ d['identifier'] for d in self.research_dataset['directories'] ] - base_paths = Directory.objects.filter(identifier__in=dir_identifiers).values_list('directory_path', flat=True) + dir_identifiers = [d["identifier"] for d in self.research_dataset["directories"]] + base_paths = Directory.objects.filter(identifier__in=dir_identifiers).values_list( + "directory_path", flat=True + ) - if dir.directory_path not in base_paths and \ - not any([ dir.directory_path.startswith(f'{p}/') for p in base_paths ]): - raise Http400(f'Directory \'{dir_identifier}\' is not included in this dataset') + if dir.directory_path not in base_paths and not any( + [dir.directory_path.startswith(f"{p}/") for p in base_paths] + ): + raise Http400(f"Directory '{dir_identifier}' is not included in this dataset") added_file_ids = self._find_new_files_added_to_dir(dir) if not added_file_ids: - _logger.info('no change in directory content') + _logger.info("no change in directory content") return (False, 0) - _logger.info(f'refreshing directory adds {len(added_file_ids)} files to dataset') + _logger.info(f"refreshing directory adds {len(added_file_ids)} files to dataset") self.date_modified = get_tz_aware_now_without_micros() self.service_modified = self.request.user.username if self.request.user.is_service else None @@ -2553,19 +2925,19 @@ def refresh_directory_content(self, dir_identifier): super(Common, new_version).save() # add all files from previous version in addition to new ones - new_version.files.add(*added_file_ids, *self.files.values_list('id', flat=True)) + new_version.files.add(*added_file_ids, *self.files.values_list("id", flat=True)) self._new_version = new_version self._create_new_dataset_version() super().save() - self.add_post_request_callable(RabbitMQPublishRecord(self, 'update')) + self.add_post_request_callable(RabbitMQPublishRecord(self, "update")) return (self.cumulative_state != self.CUMULATIVE_STATE_YES, len(added_file_ids)) def _find_new_files_added_to_dir(self, dir): - sql_insert_newly_frozen_files_by_dir_path = ''' + sql_insert_newly_frozen_files_by_dir_path = """ select f.id from metax_api_file as f where f.active = true and f.removed = false @@ -2576,12 +2948,16 @@ def _find_new_files_added_to_dir(self, dir): metax_api_catalogrecord_files cr_f where catalogrecord_id = %s ) - ''' - sql_params_insert_new_files = [dir.project_identifier, dir.directory_path, self.id] + """ + sql_params_insert_new_files = [ + dir.project_identifier, + dir.directory_path, + self.id, + ] with connection.cursor() as cr: cr.execute(sql_insert_newly_frozen_files_by_dir_path, sql_params_insert_new_files) - added_file_ids = [ v[0] for v in cr.fetchall() ] + added_file_ids = [v[0] for v in cr.fetchall()] return added_file_ids @@ -2598,33 +2974,39 @@ def fix_deprecated(self): self._copy_undeleted_files_from_old_version() self._create_new_dataset_version() super().save() - self.add_post_request_callable(RabbitMQPublishRecord(self, 'update')) + self.add_post_request_callable(RabbitMQPublishRecord(self, "update")) def _fix_deprecated_research_dataset(self): - if self.research_dataset.get('files'): - pid_list = [ f['identifier'] for f in self.research_dataset['files'] ] - pid_list_fixed = File.objects.filter(identifier__in=pid_list).values_list('identifier', flat=True) + if self.research_dataset.get("files"): + pid_list = [f["identifier"] for f in self.research_dataset["files"]] + pid_list_fixed = File.objects.filter(identifier__in=pid_list).values_list( + "identifier", flat=True + ) if len(pid_list_fixed) != len(pid_list): - self.research_dataset['files'] = [ - f for f in self.research_dataset['files'] if f['identifier'] in pid_list_fixed + self.research_dataset["files"] = [ + f for f in self.research_dataset["files"] if f["identifier"] in pid_list_fixed ] - if not self.research_dataset['files']: - del self.research_dataset['files'] + if not self.research_dataset["files"]: + del self.research_dataset["files"] - if self.research_dataset.get('directories'): - pid_list = [ d['identifier'] for d in self.research_dataset['directories'] ] - pid_list_fixed = Directory.objects.filter(identifier__in=pid_list).values_list('identifier', flat=True) + if self.research_dataset.get("directories"): + pid_list = [d["identifier"] for d in self.research_dataset["directories"]] + pid_list_fixed = Directory.objects.filter(identifier__in=pid_list).values_list( + "identifier", flat=True + ) if len(pid_list_fixed) != len(pid_list): - self.research_dataset['directories'] = [ - d for d in self.research_dataset['directories'] if d['identifier'] in pid_list_fixed + self.research_dataset["directories"] = [ + d + for d in self.research_dataset["directories"] + if d["identifier"] in pid_list_fixed ] - if not self.research_dataset['directories']: - del self.research_dataset['directories'] + if not self.research_dataset["directories"]: + del self.research_dataset["directories"] def _copy_undeleted_files_from_old_version(self): - copy_undeleted_files_sql = ''' + copy_undeleted_files_sql = """ insert into metax_api_catalogrecord_files (catalogrecord_id, file_id) select %s as catalogrecord_id, file_id from metax_api_catalogrecord_files as cr_f @@ -2637,18 +3019,22 @@ def _copy_undeleted_files_from_old_version(self): where catalogrecord_id = %s ) returning file_id - ''' - sql_params_copy_undeleted = [self._new_version.id, self.id, self._new_version.id] + """ + sql_params_copy_undeleted = [ + self._new_version.id, + self.id, + self._new_version.id, + ] with connection.cursor() as cr: cr.execute(copy_undeleted_files_sql, sql_params_copy_undeleted) n_files_copied = cr.rowcount if DEBUG: - _logger.debug('Added %d files to dataset %s' % (n_files_copied, self._new_version.id)) + _logger.debug("Added %d files to dataset %s" % (n_files_copied, self._new_version.id)) -class RabbitMQPublishRecord(): +class RabbitMQPublishRecord: """ Callable object to be passed to CommonService.add_post_request_callable(callable). @@ -2657,7 +3043,11 @@ class RabbitMQPublishRecord(): """ def __init__(self, cr, routing_key): - assert routing_key in ('create', 'update', 'delete'), 'invalid value for routing_key' + assert routing_key in ( + "create", + "update", + "delete", + ), "invalid value for routing_key" self.cr = cr self.routing_key = routing_key @@ -2668,34 +3058,35 @@ def __call__(self): from metax_api.services import RabbitMQService as rabbitmq _logger.info( - 'Publishing CatalogRecord %s to RabbitMQ... routing_key: %s' + "Publishing CatalogRecord %s to RabbitMQ... routing_key: %s" % (self.cr.identifier, self.routing_key) ) - if self.routing_key == 'delete': - cr_json = { 'identifier': self.cr.identifier } + if self.routing_key == "delete": + cr_json = {"identifier": self.cr.identifier} else: cr_json = self._to_json() # Send full data_catalog json - cr_json['data_catalog'] = {'catalog_json': self.cr.data_catalog.catalog_json} + cr_json["data_catalog"] = {"catalog_json": self.cr.data_catalog.catalog_json} try: for exchange in settings.RABBITMQ["EXCHANGES"]: - rabbitmq.publish(cr_json, routing_key=self.routing_key, exchange=exchange["NAME"]) + if exchange["EXC_TYPE"] == "dataset": + rabbitmq.publish(cr_json, routing_key=self.routing_key, exchange=exchange["NAME"]) except: # note: if we'd like to let the request be a success even if this operation fails, # we could simply not raise an exception here. - _logger.exception('Publishing rabbitmq message failed') - raise Http503({ 'detail': [ - 'failed to publish updates to rabbitmq. request is aborted.' - ]}) + _logger.exception("Publishing rabbitmq message failed") + raise Http503( + {"detail": ["failed to publish updates to rabbitmq. request is aborted."]} + ) def _to_json(self): serializer_class = self.cr.serializer_class return serializer_class(self.cr).data -class REMSUpdate(): +class REMSUpdate: """ Callable object to be passed to CommonService.add_post_request_callable(callable). @@ -2705,11 +3096,12 @@ class REMSUpdate(): def __init__(self, cr, action, **kwargs): from metax_api.services.rems_service import REMSService - assert action in ('close', 'create', 'update'), 'invalid value for action' + + assert action in ("close", "create", "update"), "invalid value for action" self.cr = cr - self.user_info = kwargs.get('user_info') - self.reason = kwargs.get('reason') - self.rems_id = kwargs.get('rems_id') + self.user_info = kwargs.get("user_info") + self.reason = kwargs.get("reason") + self.rems_id = kwargs.get("rems_id") self.action = action self.rems = REMSService() @@ -2723,26 +3115,24 @@ def __call__(self): it can be changed before saving and rems_service still knows what the resource identifier is. """ _logger.info( - 'Publishing CatalogRecord %s update to REMS... action: %s' + "Publishing CatalogRecord %s update to REMS... action: %s" % (self.cr.identifier, self.action) ) try: - if self.action == 'create': + if self.action == "create": self.rems.create_rems_entity(self.cr, self.user_info) - elif self.action == 'close': + elif self.action == "close": self.rems.close_rems_entity(self.rems_id, self.reason) - elif self.action == 'update': + elif self.action == "update": self.rems.update_rems_entity(self.cr, self.rems_id, self.reason) except Exception as e: _logger.error(e) - raise Http503({ 'detail': [ - 'failed to publish updates to rems. request is aborted.' - ]}) + raise Http503({"detail": ["failed to publish updates to rems. request is aborted."]}) -class DataciteDOIUpdate(): +class DataciteDOIUpdate: """ Callable object to be passed to CommonService.add_post_request_callable(callable). @@ -2759,7 +3149,8 @@ def __init__(self, cr, doi_identifier, action): :param action: """ from metax_api.services.datacite_service import DataciteService - assert action in ('create', 'update', 'delete'), 'invalid value for action' + + assert action in ("create", "update", "delete"), "invalid value for action" assert is_metax_generated_doi_identifier(doi_identifier) self.cr = cr self.doi_identifier = doi_identifier @@ -2772,44 +3163,50 @@ def __call__(self): Do not run for tests or in travis """ - if hasattr(settings, 'DATACITE'): - if not settings.DATACITE.get('ETSIN_URL_TEMPLATE', None): - raise Exception('Missing configuration from settings for DATACITE: ETSIN_URL_TEMPLATE') + if hasattr(settings, "DATACITE"): + if not settings.DATACITE.get("ETSIN_URL_TEMPLATE", None): + raise Exception( + "Missing configuration from settings for DATACITE: ETSIN_URL_TEMPLATE" + ) else: - raise Exception('Missing configuration from settings: DATACITE') + raise Exception("Missing configuration from settings: DATACITE") doi = extract_doi_from_doi_identifier(self.doi_identifier) if doi is None: return - if self.action == 'create': + if self.action == "create": _logger.info( - 'Publishing CatalogRecord {0} metadata and url to Datacite API using DOI {1}'. - format(self.cr.identifier, doi) + "Publishing CatalogRecord {0} metadata and url to Datacite API using DOI {1}".format( + self.cr.identifier, doi + ) ) - elif self.action == 'update': + elif self.action == "update": _logger.info( - 'Updating CatalogRecord {0} metadata and url to Datacite API using DOI {1}'. - format(self.cr.identifier, doi) + "Updating CatalogRecord {0} metadata and url to Datacite API using DOI {1}".format( + self.cr.identifier, doi + ) ) - elif self.action == 'delete': + elif self.action == "delete": _logger.info( - 'Deleting CatalogRecord {0} metadata from Datacite API using DOI {1}'. - format(self.cr.identifier, doi) + "Deleting CatalogRecord {0} metadata from Datacite API using DOI {1}".format( + self.cr.identifier, doi + ) ) from metax_api.services.datacite_service import DataciteException + try: - if self.action == 'create': + if self.action == "create": try: self._publish_to_datacite(doi) except Exception as e: # Try to delete DOI in case the DOI got created but stayed in "draft" state self.dcs.delete_draft_doi(doi) - raise(Exception(e)) - elif self.action == 'update': + raise (Exception(e)) + elif self.action == "update": self._publish_to_datacite(doi) - elif self.action == 'delete': + elif self.action == "delete": # If metadata is in "findable" state, the operation below should transition the DOI to "registered" # state self.dcs.delete_doi_metadata(doi) @@ -2818,13 +3215,14 @@ def __call__(self): raise Http400(str(e)) except Exception as e: _logger.error(e) - _logger.exception('Datacite API interaction failed') - raise Http503({'detail': [ - 'failed to publish updates to Datacite API. request is aborted.' - ]}) + _logger.exception("Datacite API interaction failed") + raise Http503( + {"detail": ["failed to publish updates to Datacite API. request is aborted."]} + ) def _publish_to_datacite(self, doi): from metax_api.services.datacite_service import convert_cr_to_datacite_cr_json + cr_json = convert_cr_to_datacite_cr_json(self.cr) datacite_xml = self.dcs.convert_catalog_record_to_datacite_xml(cr_json, True, True) _logger.debug("Datacite XML to be sent to Datacite API: {0}".format(datacite_xml)) @@ -2832,4 +3230,4 @@ def _publish_to_datacite(self, doi): # When the two operations below are successful, it should result in the DOI transitioning to # "findable" state self.dcs.create_doi_metadata(datacite_xml) - self.dcs.register_doi_url(doi, settings.DATACITE['ETSIN_URL_TEMPLATE'] % self.cr.identifier) + self.dcs.register_doi_url(doi, settings.DATACITE["ETSIN_URL_TEMPLATE"] % self.cr.identifier) diff --git a/src/metax_api/models/catalog_record_v2.py b/src/metax_api/models/catalog_record_v2.py index f35ad5b7..b9ee5895 100755 --- a/src/metax_api/models/catalog_record_v2.py +++ b/src/metax_api/models/catalog_record_v2.py @@ -10,7 +10,7 @@ from copy import deepcopy from django.conf import settings -from django.db.models import Q +from django.db.models import Q, Sum from rest_framework.serializers import ValidationError from metax_api.exceptions import Http400, Http403 @@ -24,7 +24,12 @@ get_tz_aware_now_without_micros, ) -from .catalog_record import CatalogRecord, DataciteDOIUpdate, DatasetVersionSet, RabbitMQPublishRecord +from .catalog_record import ( + CatalogRecord, + DataciteDOIUpdate, + DatasetVersionSet, + RabbitMQPublishRecord, +) from .common import Common from .directory import Directory from .file import File @@ -49,7 +54,6 @@ class CatalogRecordV2(CatalogRecord): - class Meta: # CatalogRecordV2 operates on the same database table as CatalogRecord model. Only the class # behaviour may differ from base class. @@ -58,6 +62,7 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from metax_api.api.rest.v2.serializers import CatalogRecordSerializerV2 + self.serializer_class = CatalogRecordSerializerV2 self.api_version = 2 @@ -66,69 +71,80 @@ def save(self, *args, **kwargs): Note: keys are popped from kwargs, because super().save() will complain if it receives unknown keyword arguments. """ - pid_type = kwargs.pop('pid_type', None) + pid_type = kwargs.pop("pid_type", None) if self._operation_is_create(): self._pre_create_operations() super(CatalogRecord, self).save(*args, **kwargs) self._post_create_operations(pid_type=pid_type) else: - self._pre_update_operations(draft_publish=kwargs.pop('draft_publish', False)) + self._pre_update_operations(draft_publish=kwargs.pop("draft_publish", False)) super(CatalogRecord, self).save(*args, **kwargs) self._post_update_operations() def delete(self, *args, **kwargs): + if kwargs.get("hard"): + super().delete(*args, **kwargs) + return self.id + if self.next_draft: self.next_draft.delete() self.next_draft = None elif self.is_draft_for_another_dataset(): draft_of = self.draft_of draft_of.next_draft = None - super(CatalogRecord, draft_of).save(update_fields=['next_draft']) + super(CatalogRecord, draft_of).save(update_fields=["next_draft"]) super().delete(*args, **kwargs) def _pre_create_operations(self): - if not self._check_catalog_permissions(self.data_catalog.catalog_record_group_create, - self.data_catalog.catalog_record_services_create): - raise Http403({ 'detail': [ 'You are not permitted to create datasets in this data catalog.' ]}) + if not self._check_catalog_permissions( + self.data_catalog.catalog_record_group_create, + self.data_catalog.catalog_record_services_create, + ): + raise Http403( + {"detail": ["You are not permitted to create datasets in this data catalog."]} + ) - self.research_dataset['metadata_version_identifier'] = generate_uuid_identifier() + self.research_dataset["metadata_version_identifier"] = generate_uuid_identifier() self.identifier = generate_uuid_identifier() + if not ("files" in self.research_dataset or "directories" in self.research_dataset) and "total_files_byte_size" in self.research_dataset: + self.research_dataset.pop("total_files_byte_size") + if not self._save_as_draft(): self._generate_issued_date() self._set_api_version() def _post_create_operations(self, pid_type=None): - if 'files' in self.research_dataset or 'directories' in self.research_dataset: + if "files" in self.research_dataset or "directories" in self.research_dataset: # files must be added after the record itself has been created, to be able # to insert into a many2many relation. file_changes = { - 'files': self.research_dataset.get('files', []), - 'directories': self.research_dataset.get('directories', []), + "files": self.research_dataset.get("files", []), + "directories": self.research_dataset.get("directories", []), } self.change_files(file_changes, operation_is_create=True) if self._save_as_draft(): - _logger.debug('Saving new dataset as draft') + _logger.debug("Saving new dataset as draft") - self.research_dataset['preferred_identifier'] = 'draft:%s' % self.identifier + self.research_dataset["preferred_identifier"] = "draft:%s" % self.identifier if self._get_preferred_identifier_type_from_request() == IdentifierType.DOI: self.use_doi_for_published = True else: self.use_doi_for_published = False - super(Common, self).save(update_fields=['research_dataset', 'use_doi_for_published']) + super(Common, self).save(update_fields=["research_dataset", "use_doi_for_published"]) _logger.info( - 'Created a new ' + "Created a new " % (self.id, self.identifier) ) @@ -138,24 +154,24 @@ def _post_create_operations(self, pid_type=None): # logs correctly whether dataset is created into draft state, or also published log_args = { - 'catalogrecord': { - 'identifier': self.identifier, - 'preferred_identifier': self.preferred_identifier, - 'data_catalog': self.data_catalog.catalog_json['identifier'], - 'date_created': datetime_to_str(self.date_created), - 'metadata_owner_org': self.metadata_owner_org, - 'state': self.state, + "catalogrecord": { + "identifier": self.identifier, + "preferred_identifier": self.preferred_identifier, + "data_catalog": self.data_catalog.catalog_json["identifier"], + "date_created": datetime_to_str(self.date_created), + "metadata_owner_org": self.metadata_owner_org, + "state": self.state, }, - 'user_id': self.user_created or self.service_created, + "user_id": self.user_created or self.service_created, } self.add_post_request_callable(DelayedLog(**log_args)) def is_draft_for_another_dataset(self): - return hasattr(self, 'draft_of') and self.draft_of is not None + return hasattr(self, "draft_of") and self.draft_of is not None def has_next_draft(self): - return hasattr(self, 'next_draft') and self.next_draft is not None + return hasattr(self, "next_draft") and self.next_draft is not None def _save_as_draft(self): """ @@ -163,7 +179,8 @@ def _save_as_draft(self): and to be able to differentiate between v1 and v2 drafts. """ from metax_api.services import CommonService - return CommonService.get_boolean_query_param(self.request, 'draft') + + return CommonService.get_boolean_query_param(self.request, "draft") def publish_dataset(self, pid_type=None): """ @@ -178,26 +195,28 @@ def publish_dataset(self, pid_type=None): Note: The last three steps are executed at the very end of the HTTP request, but they are queued in this method. """ - _logger.info('Publishing dataset...') + _logger.info("Publishing dataset...") if self.state == self.STATE_PUBLISHED: - raise Http400('Dataset is already published.') + raise Http400("Dataset is already published.") elif self.is_draft_for_another_dataset(): raise Http400( - 'This dataset is a draft for another published dataset. To publish the draft changes, ' - 'use API /rpc/v2/datasets/merge_draft' + "This dataset is a draft for another published dataset. To publish the draft changes, " + "use API /rpc/v2/datasets/merge_draft" ) elif self.catalog_is_dft(): - raise Http400('Cannot publish dataset in draft catalog. ' - 'Please use parameter ?draft for adding a draft dataset') + raise Http400( + "Cannot publish dataset in draft catalog. " + "Please use parameter ?draft for adding a draft dataset" + ) self.state = self.STATE_PUBLISHED self._generate_issued_date() if self.catalog_is_pas(): - _logger.debug('Catalog is PAS - Using DOI as pref_id_type') + _logger.debug("Catalog is PAS - Using DOI as pref_id_type") # todo: default identifier type could probably be a parameter of the data catalog pref_id_type = IdentifierType.DOI elif self.use_doi_for_published is True: @@ -208,53 +227,59 @@ def publish_dataset(self, pid_type=None): self.use_doi_for_published = None if self.catalog_is_harvested(): - _logger.debug('Note: Catalog is harvested') + _logger.debug("Note: Catalog is harvested") # in harvested catalogs, the harvester is allowed to set the preferred_identifier. # do not overwrite. pass elif self.catalog_is_legacy(): - if 'preferred_identifier' not in self.research_dataset: + if "preferred_identifier" not in self.research_dataset: raise Http400( - 'Selected catalog %s is a legacy catalog. Preferred identifiers are not ' - 'automatically generated for datasets stored in legacy catalogs, nor is ' - 'their uniqueness enforced. Please provide a value for dataset field ' - 'preferred_identifier.' % self.data_catalog.catalog_json['identifier'] + "Selected catalog %s is a legacy catalog. Preferred identifiers are not " + "automatically generated for datasets stored in legacy catalogs, nor is " + "their uniqueness enforced. Please provide a value for dataset field " + "preferred_identifier." % self.data_catalog.catalog_json["identifier"] ) _logger.info( - 'Catalog %s is a legacy catalog - not generating pid' - % self.data_catalog.catalog_json['identifier'] + "Catalog %s is a legacy catalog - not generating pid" + % self.data_catalog.catalog_json["identifier"] ) else: if pref_id_type == IdentifierType.URN: - self.research_dataset['preferred_identifier'] = generate_uuid_identifier(urn_prefix=True) + self.research_dataset["preferred_identifier"] = generate_uuid_identifier( + urn_prefix=True + ) elif pref_id_type == IdentifierType.DOI: if not (self.catalog_is_ida() or self.catalog_is_pas()): raise Http400("Cannot create DOI for other than datasets in IDA or PAS catalog") - _logger.debug('pref_id_type == %s, generating doi' % pref_id_type) + _logger.debug("pref_id_type == %s, generating doi" % pref_id_type) doi_id = generate_doi_identifier() - self.research_dataset['preferred_identifier'] = doi_id + self.research_dataset["preferred_identifier"] = doi_id self.preservation_identifier = doi_id else: - _logger.info("Identifier type not specified in the request. Using URN identifier for pref id") + _logger.info( + "Identifier type not specified in the request. Using URN identifier for pref id" + ) # todo better to raise validation error instead - self.research_dataset['preferred_identifier'] = generate_uuid_identifier(urn_prefix=True) + self.research_dataset["preferred_identifier"] = generate_uuid_identifier( + urn_prefix=True + ) if not self.metadata_owner_org: # field metadata_owner_org is optional, but must be set. in case it is omitted, # derive from metadata_provider_org. self.metadata_owner_org = self.metadata_provider_org - if 'remote_resources' in self.research_dataset: + if "remote_resources" in self.research_dataset: self._calculate_total_remote_resources_byte_size() if self.cumulative_state == self.CUMULATIVE_STATE_CLOSED: - raise Http400('Cannot publish cumulative dataset with state closed') + raise Http400("Cannot publish cumulative dataset with state closed") elif self.cumulative_state == self.CUMULATIVE_STATE_YES: if self.preservation_state > self.PRESERVATION_STATE_INITIALIZED: - raise Http400('Dataset cannot be cumulative if it is in PAS process') + raise Http400("Dataset cannot be cumulative if it is in PAS process") self.date_cumulation_started = self.date_created @@ -273,19 +298,21 @@ def publish_dataset(self, pid_type=None): self._validate_cr_against_datacite_schema() self.add_post_request_callable( - DataciteDOIUpdate(self, self.research_dataset['preferred_identifier'], 'create') + DataciteDOIUpdate(self, self.research_dataset["preferred_identifier"], "create") ) - if self._dataset_has_rems_managed_access() and settings.REMS['ENABLED']: + if self._dataset_has_rems_managed_access() and settings.REMS["ENABLED"]: self._pre_rems_creation() - if self.api_version != self.api_meta['version']: + if self.api_version != self.api_meta["version"]: self._set_api_version() try: # drafts are validated with different schema until the publication so check that final result is valid # according to the actual data catalog. - serializer = self.serializer_class(self, context={'request': self.request}, data=self._initial_data) + serializer = self.serializer_class( + self, context={"request": self.request}, data=self._initial_data + ) serializer.validate_json_schema(self.research_dataset) except ValidationError as e: # apierrors couldn't handle the validation error thrown by the serializer @@ -294,11 +321,11 @@ def publish_dataset(self, pid_type=None): super(Common, self).save() _logger.info( - 'Published ' + "Published " % (self.id, self.identifier, self.preferred_identifier) ) - self.add_post_request_callable(RabbitMQPublishRecord(self, 'create')) + self.add_post_request_callable(RabbitMQPublishRecord(self, "create")) def merge_draft(self): """ @@ -306,10 +333,10 @@ def merge_draft(self): published dataset. The draft record is destroyed once changes have been successfully merged. """ - _logger.info('Publishing changes from draft to a published record...') + _logger.info("Publishing changes from draft to a published record...") if not self.is_draft_for_another_dataset(): - raise Http400('Dataset is not a draft for another published dataset.') + raise Http400("Dataset is not a draft for another published dataset.") # by default retrieves the CatalogRecord object (not CatalogRecordV2!!) which behaves differently!! origin_cr = CatalogRecordV2.objects.get(next_draft_id=self.id) @@ -319,7 +346,10 @@ def merge_draft(self): origin_cr.date_modified = get_tz_aware_now_without_micros() origin_cr.user_modified = draft_cr.user_modified - if origin_cr.cumulative_state == self.CUMULATIVE_STATE_YES or origin_cr._files_added_for_first_time(): + if ( + origin_cr.cumulative_state == self.CUMULATIVE_STATE_YES + or origin_cr._files_added_for_first_time() + ): # ^ these checks should already be in place when files are added using change_files() method, # but checking here again. @@ -328,20 +358,20 @@ def merge_draft(self): if self.draft_of.removed: _logger.info( - 'Origin dataset is marked as removed - merging other changes to the published dataset, ' - 'but not adding files' + "Origin dataset is marked as removed - merging other changes to the published dataset, " + "but not adding files" ) elif self.draft_of.deprecated: _logger.info( - 'Origin dataset is deprecated - merging other changes to the published dataset, ' - 'but not adding files' + "Origin dataset is deprecated - merging other changes to the published dataset, " + "but not adding files" ) elif draft_files_count > origin_files_count: # ^ note: it should not be possible that the draft has lesser files, since # removing files is not permitted in any case. _logger.info( - 'Draft record has %d new files. Adding files to published record' + "Draft record has %d new files. Adding files to published record" % (draft_files_count - origin_files_count) ) @@ -351,16 +381,18 @@ def merge_draft(self): # files should now match, so it should be ok to just copy the directory data for file browsing origin_cr._directory_data = draft_cr._directory_data - elif draft_files_count < origin_files_count: # pragma: no cover + elif draft_files_count < origin_files_count: # pragma: no cover # should never happen - raise Exception('Files have been removed from the draft? This should not have been permitted') + raise Exception( + "Files have been removed from the draft? This should not have been permitted" + ) else: - _logger.info('No new files to add in draft') + _logger.info("No new files to add in draft") if self.draft_of.deprecated: raise Http400( - 'The origin dataset of this draft is deprecated. Changing files of a deprecated ' - 'dataset is not permitted. Please create a new dataset version first.' + "The origin dataset of this draft is deprecated. Changing files of a deprecated " + "dataset is not permitted. Please create a new dataset version first." ) # cumulative period can be closed. opening it is prevented through @@ -369,9 +401,9 @@ def merge_draft(self): # replace the "draft:" of the draft with the original pid so that # the save will not raise errors about it - draft_cr.research_dataset['preferred_identifier'] = origin_cr.preferred_identifier + draft_cr.research_dataset["preferred_identifier"] = origin_cr.preferred_identifier - if 'issued' not in draft_cr.research_dataset: + if "issued" not in draft_cr.research_dataset: draft_cr._generate_issued_date() # other than that, all values from research_dataset should be copied over @@ -392,80 +424,91 @@ def merge_draft(self): def _pre_update_operations(self, draft_publish=False): - if not self._check_catalog_permissions(self.data_catalog.catalog_record_group_edit, - self.data_catalog.catalog_record_services_edit): - raise Http403({ 'detail': [ 'You are not permitted to edit datasets in this data catalog.' ]}) + if not self._check_catalog_permissions( + self.data_catalog.catalog_record_group_edit, + self.data_catalog.catalog_record_services_edit, + ): + raise Http403( + {"detail": ["You are not permitted to edit datasets in this data catalog."]} + ) - if self.field_changed('api_meta'): - self.api_meta = self._initial_data['api_meta'] + if self.field_changed("api_meta"): + self.api_meta = self._initial_data["api_meta"] self._set_api_version() - if self.field_changed('identifier'): + if self.field_changed("identifier"): # read-only - self.identifier = self._initial_data['identifier'] + self.identifier = self._initial_data["identifier"] - if self.field_changed('research_dataset.metadata_version_identifier'): + if self.field_changed("research_dataset.metadata_version_identifier"): # read-only - self.research_dataset['metadata_version_identifier'] = \ - self._initial_data['research_dataset']['metadata_version_identifier'] + self.research_dataset["metadata_version_identifier"] = self._initial_data[ + "research_dataset" + ]["metadata_version_identifier"] - if self.field_changed('research_dataset.preferred_identifier'): + if self.field_changed("research_dataset.preferred_identifier"): if not (self.catalog_is_harvested() or self.catalog_is_legacy()): - raise Http400("Cannot change preferred_identifier in datasets in non-harvested catalogs") + raise Http400( + "Cannot change preferred_identifier in datasets in non-harvested catalogs" + ) - if self.field_changed('research_dataset.total_files_byte_size'): + if self.field_changed("research_dataset.total_files_byte_size"): if draft_publish: # allow update when merging changes from draft to published record pass - elif 'total_files_byte_size' in self._initial_data['research_dataset']: + elif "total_files_byte_size" in self._initial_data["research_dataset"]: # read-only - self.research_dataset['total_files_byte_size'] = \ - self._initial_data['research_dataset']['total_files_byte_size'] + self.research_dataset["total_files_byte_size"] = self._initial_data[ + "research_dataset" + ]["total_files_byte_size"] else: - self.research_dataset.pop('total_files_byte_size') + self.research_dataset.pop("total_files_byte_size") - if self.field_changed('research_dataset.total_remote_resources_byte_size'): + if self.field_changed("research_dataset.total_remote_resources_byte_size"): if draft_publish: # allow update when merging changes from draft to published record pass - elif 'total_remote_resources_byte_size' in self._initial_data['research_dataset']: + elif "total_remote_resources_byte_size" in self._initial_data["research_dataset"]: # read-only - self.research_dataset['total_remote_resources_byte_size'] = \ - self._initial_data['research_dataset']['total_remote_resources_byte_size'] + self.research_dataset["total_remote_resources_byte_size"] = self._initial_data[ + "research_dataset" + ]["total_remote_resources_byte_size"] else: - self.research_dataset.pop('total_remote_resources_byte_size') + self.research_dataset.pop("total_remote_resources_byte_size") - if self.field_changed('preservation_state'): + if self.field_changed("preservation_state"): if self.cumulative_state == self.CUMULATIVE_STATE_YES: - raise Http400('Changing preservation state is not allowed while dataset cumulation is active') + raise Http400( + "Changing preservation state is not allowed while dataset cumulation is active" + ) self._handle_preservation_state_changed() - if self.field_changed('deprecated') and self._initial_data['deprecated'] is True: + if self.field_changed("deprecated") and self._initial_data["deprecated"] is True: raise Http400("Cannot change dataset deprecation state from true to false") - if self.field_changed('date_deprecated') and self._initial_data['date_deprecated']: + if self.field_changed("date_deprecated") and self._initial_data["date_deprecated"]: raise Http400("Cannot change dataset deprecation date when it has been once set") - if self.field_changed('preservation_identifier'): - self.preservation_identifier = self._initial_data['preservation_identifier'] + if self.field_changed("preservation_identifier"): + self.preservation_identifier = self._initial_data["preservation_identifier"] if not self.metadata_owner_org: # can not be updated to null - self.metadata_owner_org = self._initial_data['metadata_owner_org'] + self.metadata_owner_org = self._initial_data["metadata_owner_org"] - if self.field_changed('metadata_provider_org'): + if self.field_changed("metadata_provider_org"): # read-only after creating - self.metadata_provider_org = self._initial_data['metadata_provider_org'] + self.metadata_provider_org = self._initial_data["metadata_provider_org"] - if self.field_changed('metadata_provider_user'): + if self.field_changed("metadata_provider_user"): # read-only after creating - self.metadata_provider_user = self._initial_data['metadata_provider_user'] + self.metadata_provider_user = self._initial_data["metadata_provider_user"] - if settings.REMS['ENABLED']: + if settings.REMS["ENABLED"]: self._pre_update_handle_rems() - if self.field_changed('cumulative_state'): + if self.field_changed("cumulative_state"): if draft_publish: # let cumulative state be updated if it is being changed when # draft record is being merged into a published record. @@ -477,10 +520,10 @@ def _pre_update_operations(self, draft_publish=False): "Use API /rpc/v2/datasets/change_cumulative_state to change cumulative state." ) - if self.field_changed('data_catalog_id'): + if self.field_changed("data_catalog_id"): if self.catalog_is_att(self._initial_data) and self.catalog_is_ida(): - self.research_dataset.pop('remote_resources') - self.research_dataset.pop('total_remote_resources_byte_size') + self.research_dataset.pop("remote_resources") + self.research_dataset.pop("total_remote_resources_byte_size") self._handle_metadata_versioning() if draft_publish: @@ -492,29 +535,36 @@ def _pre_update_operations(self, draft_publish=False): # be updated by using the api /rest/v2/datasets/pid/files/user_metadata for existing # files, or by using /rest/v2/datasets/pid/files for adding new files with user metadata. - if self.research_dataset.get('files') != self._initial_data['research_dataset'].get('files'): - if 'files' in self._initial_data['research_dataset']: - self.research_dataset['files'] = self._initial_data['research_dataset']['files'] + if self.research_dataset.get("files") != self._initial_data["research_dataset"].get( + "files" + ): + if "files" in self._initial_data["research_dataset"]: + self.research_dataset["files"] = self._initial_data["research_dataset"]["files"] else: - del self.research_dataset['files'] - - if self.research_dataset.get('directories') != self._initial_data['research_dataset'].get('directories'): - if 'directories' in self._initial_data['research_dataset']: - self.research_dataset['directories'] = self._initial_data['research_dataset']['directories'] + del self.research_dataset["files"] + + if self.research_dataset.get("directories") != self._initial_data[ + "research_dataset" + ].get("directories"): + if "directories" in self._initial_data["research_dataset"]: + self.research_dataset["directories"] = self._initial_data["research_dataset"][ + "directories" + ] else: - del self.research_dataset['directories'] + del self.research_dataset["directories"] - if self.field_changed('research_dataset') and self.state == self.STATE_PUBLISHED: + if self.field_changed("research_dataset") and self.state == self.STATE_PUBLISHED: self.update_datacite = True if self.preservation_state in ( - self.PRESERVATION_STATE_INVALID_METADATA, # 40 - self.PRESERVATION_STATE_METADATA_VALIDATION_FAILED, # 50 - self.PRESERVATION_STATE_VALID_METADATA): # 70 + self.PRESERVATION_STATE_INVALID_METADATA, # 40 + self.PRESERVATION_STATE_METADATA_VALIDATION_FAILED, # 50 + self.PRESERVATION_STATE_VALID_METADATA, + ): # 70 # notifies the user in Hallintaliittyma that the metadata needs to be re-validated - self.preservation_state = self.PRESERVATION_STATE_VALIDATED_METADATA_UPDATED # 60 + self.preservation_state = self.PRESERVATION_STATE_VALIDATED_METADATA_UPDATED # 60 self.preservation_state_modified = self.date_modified if self.catalog_versions_datasets(): @@ -525,12 +575,20 @@ def _pre_update_operations(self, draft_publish=False): self._handle_metadata_versioning() elif self.catalog_is_harvested(): - if self.field_changed('research_dataset.preferred_identifier'): + if self.field_changed("research_dataset.preferred_identifier"): self._handle_preferred_identifier_changed() else: self.update_datacite = False + def _calculate_total_files_byte_size(self): + rd = self.research_dataset + rd["total_files_byte_size"] = 0 + if self.files.count() > 0: + rd["total_files_byte_size"] = ( + self.files.aggregate(Sum("byte_size"))["byte_size__sum"] or 0 + ) + def _update_dataset_specific_metadata(self, file_changes, operation_is_create=False): """ Take dataset-specific metadata (also knows as "user metadata") from file_changes and persist @@ -540,34 +598,37 @@ def _update_dataset_specific_metadata(self, file_changes, operation_is_create=Fa schema. For existing entries, the existing entry is updated either by replacing it, or field by field, if using PATCH. """ - _logger.debug('Updating files dataset-specific metadata...') + _logger.debug("Updating files dataset-specific metadata...") - for object_type in ('files', 'directories'): + for object_type in ("files", "directories"): if not file_changes.get(object_type): - _logger.debug('No objects of type %s - continuing' % object_type) + _logger.debug("No objects of type %s - continuing" % object_type) continue # filter in only entries that ADD files. makes no sense to keep entries that exclude stuff add_entries = [ - obj for obj in file_changes[object_type] - if obj.get('exclude', False) is False - and obj.get('delete', False) is False + obj + for obj in file_changes[object_type] + if obj.get("exclude", False) is False and obj.get("delete", False) is False ] # remove the exclude and delete -keys, if they happen to exist. after that, if the obj contains # more than 1 key (the obj identifier), we know that it must contain dataset-specific metadata too. for obj in add_entries: - obj.pop('exclude', None) - obj.pop('delete', None) + obj.pop("exclude", None) + obj.pop("delete", None) - add_entries = [ obj for obj in add_entries if len(obj) > 1 ] + add_entries = [obj for obj in add_entries if len(obj) > 1] - _logger.debug('Received %d add entries' % len(add_entries)) + _logger.debug("Received %d add entries" % len(add_entries)) if operation_is_create: - _logger.debug('Note: operation_is_create=True - replacing all %s with received entries' % object_type) + _logger.debug( + "Note: operation_is_create=True - replacing all %s with received entries" + % object_type + ) if add_entries: # if there are no entries to add, do NOT set an empty array! @@ -577,14 +638,16 @@ def _update_dataset_specific_metadata(self, file_changes, operation_is_create=Fa # else -> update type operation - _logger.debug('Update operation type = %s' % self.request.META['REQUEST_METHOD']) + _logger.debug("Update operation type = %s" % self.request.META["REQUEST_METHOD"]) # take entries that delete metadata, and process them at the end delete_entries = set( - obj['identifier'] for obj in file_changes[object_type] if obj.get('delete', False) is True + obj["identifier"] + for obj in file_changes[object_type] + if obj.get("delete", False) is True ) - _logger.debug('Received %d delete entries' % len(delete_entries)) + _logger.debug("Received %d delete entries" % len(delete_entries)) new_entries = [] @@ -597,9 +660,9 @@ def _update_dataset_specific_metadata(self, file_changes, operation_is_create=Fa for current_obj in self.research_dataset.get(object_type, []): - if received_obj['identifier'] == current_obj['identifier']: + if received_obj["identifier"] == current_obj["identifier"]: - if self.request.META['REQUEST_METHOD'] in ('POST', 'PUT'): + if self.request.META["REQUEST_METHOD"] in ("POST", "PUT"): # replace object: drop all keys, and add only received keys below since re-assigning the # current_obj a new reference does not change the actual object in the array. current_obj.clear() @@ -624,19 +687,24 @@ def _update_dataset_specific_metadata(self, file_changes, operation_is_create=Fa # finally, delete all dataset-specific metadata entries as requested self.research_dataset[object_type] = [ - obj for obj in self.research_dataset[object_type] - if obj['identifier'] not in delete_entries + obj + for obj in self.research_dataset[object_type] + if obj["identifier"] not in delete_entries ] if not self.research_dataset[object_type]: # do not leave empty arrays in the dict del self.research_dataset[object_type] - _logger.debug('Number of %s metadata entries added: %d' % (object_type, len(new_entries))) - _logger.debug('Number of %s metadata entries updated: %d' % (object_type, num_updated)) - _logger.debug('Number of %s metadata entries deleted: %d' % (object_type, len(delete_entries))) + _logger.debug( + "Number of %s metadata entries added: %d" % (object_type, len(new_entries)) + ) + _logger.debug("Number of %s metadata entries updated: %d" % (object_type, num_updated)) + _logger.debug( + "Number of %s metadata entries deleted: %d" % (object_type, len(delete_entries)) + ) - super(Common, self).save(update_fields=['research_dataset']) + super(Common, self).save(update_fields=["research_dataset"]) def _files_added_for_first_time(self): """ @@ -651,12 +719,12 @@ def _cumulative_add_check_excludes(self, file_changes): raising an error and stopping the operation altogether should be the path of least astonishment. """ - for object_type in ('files', 'directories'): + for object_type in ("files", "directories"): for obj in file_changes.get(object_type, []): - if obj.get('exclude', False) is True: + if obj.get("exclude", False) is True: raise Http400( - 'Excluding files from a cumulative dataset is not permitted. ' - 'Please create a new dataset version first.' + "Excluding files from a cumulative dataset is not permitted. " + "Please create a new dataset version first." ) def change_files(self, file_changes, operation_is_create=False): @@ -706,23 +774,25 @@ def change_files(self, file_changes, operation_is_create=False): Here we can't trust in method _operation_is_create(), since it tries to detect creating phase from presence of self.id, which in this case is already there since we are so late in the create process. """ - _logger.debug('Changing dataset included files...') - _logger.debug('Note: operation_is_create=%r' % operation_is_create) + _logger.debug("Changing dataset included files...") + _logger.debug("Note: operation_is_create=%r" % operation_is_create) if self.deprecated: raise Http400( - 'Changing files of a deprecated dataset is not permitted. Please create a new dataset version first.' + "Changing files of a deprecated dataset is not permitted. Please create a new dataset version first." ) assert type(file_changes) is dict - assert hasattr(self, 'request') and self.request is not None + assert hasattr(self, "request") and self.request is not None - if not (file_changes.get('files') or file_changes.get('directories')): - _logger.debug('Received data does not include files or directories - returning') + if not (file_changes.get("files") or file_changes.get("directories")): + _logger.debug("Received data does not include files or directories - returning") return # create an instance of the serializer for later validations - serializer = self.serializer_class(self, context={'request': self.request}, data=self._initial_data) + serializer = self.serializer_class( + self, context={"request": self.request}, data=self._initial_data + ) if operation_is_create: # todo: probably it would be best to leave this timestamp field empty on initial create @@ -734,7 +804,7 @@ def change_files(self, file_changes, operation_is_create=False): # a published dataset. draft datasets in general allow for much greater freedom # in making changes to the files of a dataset. - _logger.debug('self.state == %s' % self.state) + _logger.debug("self.state == %s" % self.state) if self.state == self.STATE_DRAFT: @@ -749,8 +819,8 @@ def change_files(self, file_changes, operation_is_create=False): if self.draft_of.deprecated: raise Http400( - 'The origin dataset of this draft is deprecated. Changing files of a deprecated ' - 'dataset is not permitted. Please create a new dataset version first.' + "The origin dataset of this draft is deprecated. Changing files of a deprecated " + "dataset is not permitted. Please create a new dataset version first." ) elif self.draft_of._files_added_for_first_time(): @@ -764,10 +834,10 @@ def change_files(self, file_changes, operation_is_create=False): self._cumulative_add_check_excludes(file_changes) else: raise Http400( - 'Changing files of a published dataset is not permitted. ' - 'Please create a new dataset version first. ' - 'If you need to continuously add new files to a published dataset, ' - 'consider creating a new cumulative dataset.' + "Changing files of a published dataset is not permitted. " + "Please create a new dataset version first. " + "If you need to continuously add new files to a published dataset, " + "consider creating a new cumulative dataset." ) else: # "normal case": a new dataset in draft state. file changes are generally @@ -781,8 +851,8 @@ def change_files(self, file_changes, operation_is_create=False): if self.next_draft: raise Http400( - 'The dataset has an existing unmerged draft. While the draft exists, new files can only be ' - 'added to the draft record.' + "The dataset has an existing unmerged draft. While the draft exists, new files can only be " + "added to the draft record." ) elif self._files_added_for_first_time(): # first update from 0 to n files should be allowed even for a published dataset, and @@ -796,10 +866,10 @@ def change_files(self, file_changes, operation_is_create=False): else: # published dataset with no special status. changing files is not permitted. raise Http400( - 'Changing files of a published dataset is not permitted. ' - 'Please create a new dataset version first. ' - 'If you need to continuously add new files to a published dataset, ' - 'consider creating a new cumulative dataset.' + "Changing files of a published dataset is not permitted. " + "Please create a new dataset version first. " + "If you need to continuously add new files to a published dataset, " + "consider creating a new cumulative dataset." ) # validating received data in this method is only necessary for update operations. for @@ -815,50 +885,63 @@ def change_files(self, file_changes, operation_is_create=False): self.date_last_cumulative_addition = self.date_modified # for counting changes at the end - files_before_set = set(id for id in self.files.all().values_list('id', flat=True)) + files_before_set = set(id for id in self.files.all().values_list("id", flat=True)) files_excluded = False - for dr in file_changes.get('directories', []): + for dr in file_changes.get("directories", []): # process directory add end exclude entries in the order they are provided files = self._get_dataset_selected_file_ids( - dr['identifier'], 'directory', exclude=dr.get('exclude', False)) + dr["identifier"], "directory", exclude=dr.get("exclude", False) + ) - if dr.get('exclude', False) is False: - _logger.debug('Found %d files to add based on received directory objects' % len(files)) + if dr.get("exclude", False) is False: + _logger.debug( + "Found %d files to add based on received directory objects" % len(files) + ) self.files.add(*files) else: - _logger.debug('Found %d files to exclude based on received directory objects' % len(files)) + _logger.debug( + "Found %d files to exclude based on received directory objects" % len(files) + ) files_excluded = True self.files.remove(*files) # process individual file add and exclude entries. order does not matter. files_add = [ - f['identifier'] for f in file_changes.get('files', []) - if f.get('exclude', False) is False + f["identifier"] + for f in file_changes.get("files", []) + if f.get("exclude", False) is False ] files_exclude = [ - f['identifier'] for f in file_changes.get('files', []) - if f.get('exclude', False) is True + f["identifier"] + for f in file_changes.get("files", []) + if f.get("exclude", False) is True ] - file_ids_add = self._get_dataset_selected_file_ids(files_add, 'file', exclude=False) - file_ids_exclude = self._get_dataset_selected_file_ids(files_exclude, 'file', exclude=True) + file_ids_add = self._get_dataset_selected_file_ids(files_add, "file", exclude=False) + file_ids_exclude = self._get_dataset_selected_file_ids(files_exclude, "file", exclude=True) - _logger.debug('Found %d files to add based on received file objects' % len(file_ids_add)) - _logger.debug('Found %d files to exclude based on received file objects' % len(file_ids_exclude)) + _logger.debug("Found %d files to add based on received file objects" % len(file_ids_add)) + _logger.debug( + "Found %d files to exclude based on received file objects" % len(file_ids_exclude) + ) self.files.add(*file_ids_add) self.files.remove(*file_ids_exclude) # do final checking that resulting dataset contains files only from a single project - projects = self.files.all().values_list('project_identifier', flat=True).distinct('project_identifier') + projects = ( + self.files.all() + .values_list("project_identifier", flat=True) + .distinct("project_identifier") + ) if len(projects) > 1: - raise Http400('All added files must be from the same project') + raise Http400("All added files must be from the same project") if file_ids_exclude: files_excluded = True @@ -870,7 +953,9 @@ def change_files(self, file_changes, operation_is_create=False): self._clear_non_included_file_metadata_entries() # when adding new files, the entries can contain dataset-specific metadata - self._update_dataset_specific_metadata(file_changes, operation_is_create=operation_is_create) + self._update_dataset_specific_metadata( + file_changes, operation_is_create=operation_is_create + ) # update total file size, and numbers for directories that are shown when browsing files self._calculate_total_files_byte_size() @@ -886,7 +971,7 @@ def change_files(self, file_changes, operation_is_create=False): super(Common, self).save() # count effect of performed actions: number of files added and excluded - file_ids_after = self.files.all().values_list('id', flat=True) + file_ids_after = self.files.all().values_list("id", flat=True) if operation_is_create: files_added_count = len(file_ids_after) @@ -896,12 +981,12 @@ def change_files(self, file_changes, operation_is_create=False): files_excluded_count = len(files_before_set.difference(files_after_set)) ret = { - 'files_added': files_added_count, + "files_added": files_added_count, } if not operation_is_create: # operation is update. for a create operation, there would never be removed files - ret['files_removed'] = files_excluded_count + ret["files_removed"] = files_excluded_count return ret @@ -909,19 +994,19 @@ def _get_dataset_selected_file_ids(self, identifier_list, identifier_type, exclu """ Return a list of ids of all unique individual files currently in the db. """ - assert identifier_type in ('file', 'directory') + assert identifier_type in ("file", "directory") if type(identifier_list) is not list: # this method can accept a single identifier too identifier_list = [identifier_list] file_ids = [] - file_changes = { 'changed_projects': defaultdict(set) } + file_changes = {"changed_projects": defaultdict(set)} - if identifier_type == 'file': + if identifier_type == "file": file_ids = self._get_file_ids_from_file_list(identifier_list, file_changes, exclude) - elif identifier_type == 'directory': + elif identifier_type == "directory": file_ids = self._get_file_ids_from_dir_list(identifier_list, file_changes, exclude) self._check_changed_files_permissions(file_changes) @@ -938,25 +1023,28 @@ def _get_file_ids_from_file_list(self, file_identifiers, file_changes, exclude): if exclude: # retrieve files for the purpose excluding files from the dataset. search only from current dataset files - files = self.files \ - .filter(identifier__in=file_identifiers) \ - .values('id', 'identifier', 'project_identifier') + files = self.files.filter(identifier__in=file_identifiers).values( + "id", "identifier", "project_identifier" + ) else: # retrieve files for the purpose of adding new files. search only from files not already part of the dataset - files = File.objects \ - .filter(identifier__in=file_identifiers) \ - .exclude(id__in=self.files.all()) \ - .values('id', 'identifier', 'project_identifier') + files = ( + File.objects.filter(identifier__in=file_identifiers) + .exclude(id__in=self.files.all()) + .values("id", "identifier", "project_identifier") + ) if len(files) != len(file_identifiers): missing_identifiers = [ - pid for pid in file_identifiers if pid not in set([f['identifier'] for f in files]) + pid for pid in file_identifiers if pid not in set([f["identifier"] for f in files]) ] # ensure these were not already included in the dataset - from_record = self.files.filter(identifier__in=missing_identifiers).values_list('identifier', flat=True) + from_record = self.files.filter(identifier__in=missing_identifiers).values_list( + "identifier", flat=True + ) if len(missing_identifiers) == len(from_record): # all files were already part of the dataset. no files to add @@ -964,16 +1052,18 @@ def _get_file_ids_from_file_list(self, file_identifiers, file_changes, exclude): # otherwise, some were actually not found - missing_identifiers = [ f for f in missing_identifiers if f not in from_record ] + missing_identifiers = [f for f in missing_identifiers if f not in from_record] - raise Http400({ - 'detail': ['Some requested files were not found. File identifiers not found:'], - 'data': missing_identifiers - }) + raise Http400( + { + "detail": ["Some requested files were not found. File identifiers not found:"], + "data": missing_identifiers, + } + ) - file_changes['changed_projects']['files_added'].add(files[0]['project_identifier']) + file_changes["changed_projects"]["files_added"].add(files[0]["project_identifier"]) - return [ f['id'] for f in files ] + return [f["id"] for f in files] def _get_file_ids_from_dir_list(self, dir_identifiers, file_changes, exclude): """ @@ -984,54 +1074,59 @@ def _get_file_ids_from_dir_list(self, dir_identifiers, file_changes, exclude): if not dir_identifiers: return [] - dirs = Directory.objects.filter(identifier__in=dir_identifiers).values('project_identifier', 'directory_path') + dirs = Directory.objects.filter(identifier__in=dir_identifiers).values( + "project_identifier", "directory_path" + ) if len(dirs) == 0: - raise Http400('no directories matched given identifiers') + raise Http400("no directories matched given identifiers") elif len(dirs) != len(dir_identifiers): missing_identifiers = [ - pid for pid in dir_identifiers if pid not in set([dr['identifier'] for dr in dirs]) + pid for pid in dir_identifiers if pid not in set([dr["identifier"] for dr in dirs]) ] - raise Http400({ - 'detail': ['Some requested directories were not found. Directory identifiers not found:'], - 'data': missing_identifiers - }) + raise Http400( + { + "detail": [ + "Some requested directories were not found. Directory identifiers not found:" + ], + "data": missing_identifiers, + } + ) - project_identifier = dirs[0]['project_identifier'] + project_identifier = dirs[0]["project_identifier"] file_filter = Q() for dr in dirs: - if dr['project_identifier'] != project_identifier: - raise Http400('All added files must be from the same project') + if dr["project_identifier"] != project_identifier: + raise Http400("All added files must be from the same project") - if dr['directory_path'] == '/': + if dr["directory_path"] == "/": file_filter = Q() break else: - file_filter |= Q(file_path__startswith='%s/' % dr['directory_path']) + file_filter |= Q(file_path__startswith="%s/" % dr["directory_path"]) # results in ((path like x or path like y...) and project = z) file_filter &= Q(project_identifier=project_identifier) - file_changes['changed_projects']['files_added'].add(project_identifier) + file_changes["changed_projects"]["files_added"].add(project_identifier) if exclude: # retrieve files for the purpose excluding files from the dataset. search only from current dataset files - file_ids = self.files \ - .filter(file_filter) \ - .values_list('id', flat=True) + file_ids = self.files.filter(file_filter).values_list("id", flat=True) else: # retrieve files for the purpose of adding new files. search only from files not already part of the dataset - file_ids = File.objects \ - .filter(file_filter) \ - .exclude(id__in=self.files.all()) \ - .values_list('id', flat=True) + file_ids = ( + File.objects.filter(file_filter) + .exclude(id__in=self.files.all()) + .values_list("id", flat=True) + ) - return [ id for id in file_ids ] + return [id for id in file_ids] def _clear_non_included_file_metadata_entries(self, raise_on_not_found=False): """ @@ -1041,73 +1136,86 @@ def _clear_non_included_file_metadata_entries(self, raise_on_not_found=False): Parameter raise_on_not_found on can be used to raise an error if any metadata entry is not actually an included file in the dataset. """ - _logger.debug('Clearing non-included file metadata entries...') - _logger.debug('Note: raise_on_not_found=%r' % raise_on_not_found) + _logger.debug("Clearing non-included file metadata entries...") + _logger.debug("Note: raise_on_not_found=%r" % raise_on_not_found) # files - file_identifiers = [ f['identifier'] for f in self.research_dataset.get('files', []) ] + file_identifiers = [f["identifier"] for f in self.research_dataset.get("files", [])] if file_identifiers: included_files = set( - idf for idf in self.files.filter(identifier__in=file_identifiers).values_list('identifier', flat=True) + idf + for idf in self.files.filter(identifier__in=file_identifiers).values_list( + "identifier", flat=True + ) ) if raise_on_not_found: # note: use of this parameter should only be relevant when updating dataset-specific metadata. - not_included_files = [ idf for idf in file_identifiers if idf not in included_files ] + not_included_files = [idf for idf in file_identifiers if idf not in included_files] if not_included_files: - raise Http400({ - 'detail': [ - 'The following files are not included in the dataset, or the files may ' - 'have been marked as removed.' - ], - 'data': not_included_files - }) - - self.research_dataset['files'] = [ - f for f in self.research_dataset['files'] if f['identifier'] in included_files + raise Http400( + { + "detail": [ + "The following files are not included in the dataset, or the files may " + "have been marked as removed." + ], + "data": not_included_files, + } + ) + + self.research_dataset["files"] = [ + f for f in self.research_dataset["files"] if f["identifier"] in included_files ] # dirs - dir_identifiers = [ dr['identifier'] for dr in self.research_dataset.get('directories', []) ] + dir_identifiers = [dr["identifier"] for dr in self.research_dataset.get("directories", [])] if dir_identifiers: current_dir_entries = [ - dr for dr - in Directory.objects.filter(identifier__in=dir_identifiers).values('identifier', 'directory_path') + dr + for dr in Directory.objects.filter(identifier__in=dir_identifiers).values( + "identifier", "directory_path" + ) ] included_dirs = set() for dr in current_dir_entries: - dir_has_files = self.files.filter(file_path__startswith='%s/' % dr['directory_path']).exists() + dir_has_files = self.files.filter( + file_path__startswith="%s/" % dr["directory_path"] + ).exists() if dir_has_files: # directory or one of its sub directories has at least one file. therefore # this directory metadata entry should remain. - included_dirs.add(dr['identifier']) + included_dirs.add(dr["identifier"]) if raise_on_not_found: # note: use of this parameter should only be relevant when updating dataset-specific metadata. - not_included_dirs = [ idf for idf in dir_identifiers if idf not in included_dirs ] + not_included_dirs = [idf for idf in dir_identifiers if idf not in included_dirs] if not_included_dirs: - raise Http400({ - 'detail': 'The following directories do not contain any files in the dataset. ' - 'Please add files to the dataset first.', - 'data': not_included_dirs - }) - - self.research_dataset['directories'] = [ - dr for dr in self.research_dataset['directories'] if dr['identifier'] in included_dirs + raise Http400( + { + "detail": "The following directories do not contain any files in the dataset. " + "Please add files to the dataset first.", + "data": not_included_dirs, + } + ) + + self.research_dataset["directories"] = [ + dr + for dr in self.research_dataset["directories"] + if dr["identifier"] in included_dirs ] def update_files_dataset_specific_metadata(self, md_changes): @@ -1115,21 +1223,21 @@ def update_files_dataset_specific_metadata(self, md_changes): Update contents of fields research_dataset.files and research_dataset.directories, i.e. "user metadata" or "dataset-specific metadata". """ - _logger.info('Updating dataset file metadata...') + _logger.info("Updating dataset file metadata...") serializer = self.serializer_class(self) # note: this does json schema validation, and its output from the api is not user friendly # at all, but its better than nothing... - if self.request.META['REQUEST_METHOD'] == 'PUT': + if self.request.META["REQUEST_METHOD"] == "PUT": # do not validate for patch, since it can contain only partial fields serializer.validate_research_dataset_files(md_changes) - for object_type in ('files', 'directories'): + for object_type in ("files", "directories"): for obj in md_changes.get(object_type, []): - if 'identifier' not in obj: - raise Http400('\'identifier\' is a required field in all metadata entries.') + if "identifier" not in obj: + raise Http400("'identifier' is a required field in all metadata entries.") self._update_dataset_specific_metadata(md_changes, operation_is_create=False) self._clear_non_included_file_metadata_entries(raise_on_not_found=True) @@ -1138,7 +1246,7 @@ def update_files_dataset_specific_metadata(self, md_changes): files_and_dirs = {} - for object_type in ('files', 'directories'): + for object_type in ("files", "directories"): if object_type in self.research_dataset: files_and_dirs[object_type] = self.research_dataset[object_type] @@ -1156,18 +1264,20 @@ def create_draft(self): """ Create a new draft of a published dataset, that can later be merged back to the original published dataset. """ - _logger.info('Creating a draft of a published dataset...') + _logger.info("Creating a draft of a published dataset...") if self.is_draft_for_another_dataset(): - raise Http400('Dataset is already a draft for another published dataset.') + raise Http400("Dataset is already a draft for another published dataset.") elif self.state == self.STATE_DRAFT: # a new dataset in draft state - raise Http400('Dataset is already draft.') + raise Http400("Dataset is already draft.") if self.next_draft: raise Http400( - 'The dataset already has an existing unmerged draft: {}'.format(self.next_draft.preferred_identifier) + "The dataset already has an existing unmerged draft: {}".format( + self.next_draft.preferred_identifier + ) ) origin_cr = self @@ -1176,7 +1286,7 @@ def create_draft(self): draft_cr.date_created = get_tz_aware_now_without_micros() draft_cr.state = self.STATE_DRAFT draft_cr.cumulative_state = origin_cr.cumulative_state - draft_cr.research_dataset['preferred_identifier'] = 'draft:%s' % draft_cr.identifier + draft_cr.research_dataset["preferred_identifier"] = "draft:%s" % draft_cr.identifier super(CatalogRecord, draft_cr).save() @@ -1192,16 +1302,16 @@ def create_draft(self): super(CatalogRecord, origin_cr).save() log_args = { - 'event': 'dataset_draft_created', - 'user_id': draft_cr.user_created or draft_cr.service_created, - 'catalogrecord': { - 'identifier': draft_cr.identifier, - 'preferred_identifier': draft_cr.preferred_identifier, - 'data_catalog': draft_cr.data_catalog.catalog_json['identifier'], - 'date_created': datetime_to_str(draft_cr.date_created), - 'metadata_owner_org': draft_cr.metadata_owner_org, - 'state': draft_cr.state, - } + "event": "dataset_draft_created", + "user_id": draft_cr.user_created or draft_cr.service_created, + "catalogrecord": { + "identifier": draft_cr.identifier, + "preferred_identifier": draft_cr.preferred_identifier, + "data_catalog": draft_cr.data_catalog.catalog_json["identifier"], + "date_created": datetime_to_str(draft_cr.date_created), + "metadata_owner_org": draft_cr.metadata_owner_org, + "state": draft_cr.state, + }, } self.add_post_request_callable(DelayedLog(**log_args)) @@ -1211,22 +1321,22 @@ def create_new_version(self): A method to "explicitly" create a new version of a dataset, which is called from a particular RPC API endpoint. """ - _logger.info('Creating a new dataset version...') + _logger.info("Creating a new dataset version...") if self.is_draft_for_another_dataset(): raise Http400( - 'Can\'t create new version. Dataset is a draft for another published dataset: %s' + "Can't create new version. Dataset is a draft for another published dataset: %s" % self.draft_of.identifier ) elif self.has_next_draft(): raise Http400( - 'Can\'t create new version. Dataset has an unmerged draft: %s' + "Can't create new version. Dataset has an unmerged draft: %s" % self.next_draft.identifier ) elif not self.catalog_versions_datasets(): - raise Http400('Data catalog does not allow dataset versioning') + raise Http400("Data catalog does not allow dataset versioning") elif self.state == self.STATE_DRAFT: - raise Http400('Cannot create new version from draft dataset') + raise Http400("Cannot create new version from draft dataset") self._new_version = self._create_new_dataset_version_template() self._create_new_dataset_version() @@ -1235,16 +1345,21 @@ def _create_new_dataset_version(self): """ Create a new dataset version of the record who calls this method. """ - assert hasattr(self, '_new_version'), 'self._new_version should have been set in a previous step' + assert hasattr( + self, "_new_version" + ), "self._new_version should have been set in a previous step" old_version = self if old_version.next_dataset_version_id: raise Http400( - 'Dataset already has a next version: %s' % old_version.next_dataset_version.identifier + "Dataset already has a next version: %s" + % old_version.next_dataset_version.identifier ) - _logger.info('Creating new dataset version from old CatalogRecord: %s' % old_version.identifier) + _logger.info( + "Creating new dataset version from old CatalogRecord: %s" % old_version.identifier + ) new_version = self._new_version new_version.state = self.STATE_DRAFT @@ -1275,10 +1390,10 @@ def _create_new_dataset_version(self): new_version.user_created = self.request.user.username new_version.research_dataset = deepcopy(old_version.research_dataset) - new_version.research_dataset['metadata_version_identifier'] = generate_uuid_identifier() + new_version.research_dataset["metadata_version_identifier"] = generate_uuid_identifier() # temporary "pid" until draft is published - new_version.research_dataset['preferred_identifier'] = 'draft:%s' % self.identifier + new_version.research_dataset["preferred_identifier"] = "draft:%s" % self.identifier if old_version.files.exists(): # copy all files from previous version to new version. @@ -1296,43 +1411,30 @@ def _create_new_dataset_version(self): old_version.dataset_version_set.records.add(new_version) old_version.next_dataset_version = new_version - if new_version.editor: - # some of the old editor fields cant be true in the new version, so keep - # only the ones that make sense. it is up to the editor, to update other fields - # they see as relevant. we also dont want null values in there - old_editor = deepcopy(new_version.editor) - new_version.editor = {} - if 'owner_id' in old_editor: - new_version.editor['owner_id'] = old_editor['owner_id'] - if 'creator_id' in old_editor: - new_version.editor['creator_id'] = old_editor['creator_id'] - if 'identifier' in old_editor: - # todo this probably does not make sense... ? - new_version.editor['identifier'] = old_editor['identifier'] - # v2 api successfully invoked, change the api version to prevent further updates on v1 api self._set_api_version() super(Common, new_version).save() super(Common, old_version).save() - _logger.info('New dataset version draft created, identifier %s' % new_version.identifier) + _logger.info("New dataset version draft created, identifier %s" % new_version.identifier) log_args = { - 'catalogrecord': { - 'identifier': new_version.identifier, - 'preferred_identifier': new_version.preferred_identifier, - 'data_catalog': new_version.data_catalog.catalog_json['identifier'], - 'date_created': datetime_to_str(new_version.date_created), - 'metadata_owner_org': new_version.metadata_owner_org, - 'state': new_version.state, + "catalogrecord": { + "identifier": new_version.identifier, + "preferred_identifier": new_version.preferred_identifier, + "data_catalog": new_version.data_catalog.catalog_json["identifier"], + "date_created": datetime_to_str(new_version.date_created), + "metadata_owner_org": new_version.metadata_owner_org, + "state": new_version.state, }, - 'user_id': new_version.user_created or new_version.service_created, + "user_id": new_version.user_created or new_version.service_created, } - log_args['event'] = 'dataset_version_created' - log_args['catalogrecord']['previous_version_preferred_identifier'] \ - = new_version.previous_dataset_version.preferred_identifier + log_args["event"] = "dataset_version_created" + log_args["catalogrecord"][ + "previous_version_preferred_identifier" + ] = new_version.previous_dataset_version.preferred_identifier self.add_post_request_callable(DelayedLog(**log_args)) @@ -1344,23 +1446,24 @@ def change_cumulative_state(self, new_state): creating a new version into draft state first. """ if self.next_dataset_version: - raise Http400('Cannot change cumulative_state on old dataset version') + raise Http400("Cannot change cumulative_state on old dataset version") - cumulative_state_valid_values = [ choice[0] for choice in self.CUMULATIVE_STATE_CHOICES ] + cumulative_state_valid_values = [choice[0] for choice in self.CUMULATIVE_STATE_CHOICES] try: new_state = int(new_state) assert new_state in cumulative_state_valid_values except: raise Http400( - 'cumulative_state must be one of: %s' % ', '.join(str(x) for x in cumulative_state_valid_values) + "cumulative_state must be one of: %s" + % ", ".join(str(x) for x in cumulative_state_valid_values) ) if self.cumulative_state == new_state: - _logger.info('No change in cumulative_state') + _logger.info("No change in cumulative_state") return - _logger.info('Changing cumulative_state from %d to %d' % (self.cumulative_state, new_state)) + _logger.info("Changing cumulative_state from %d to %d" % (self.cumulative_state, new_state)) self.date_modified = get_tz_aware_now_without_micros() self.service_modified = self.request.user.username if self.request.user.is_service else None @@ -1385,7 +1488,9 @@ def change_cumulative_state(self, new_state): self.date_last_cumulative_addition = None elif new_state == self.CUMULATIVE_STATE_CLOSED: - raise Http400('For a new dataset, cumulative_state must be \'not cumulative\' or \'open\'') + raise Http400( + "For a new dataset, cumulative_state must be 'not cumulative' or 'open'" + ) elif new_state == self.CUMULATIVE_STATE_YES: # start date is set during publishing @@ -1403,16 +1508,16 @@ def change_cumulative_state(self, new_state): if new_state == self.CUMULATIVE_STATE_NO: raise Http400( - 'Cumulative dataset cannot be set to non-cumulative dataset. ' - 'If you want to stop active cumulation, set cumulative status to closed.' + "Cumulative dataset cannot be set to non-cumulative dataset. " + "If you want to stop active cumulation, set cumulative status to closed." ) elif new_state == self.CUMULATIVE_STATE_CLOSED: if comparison_cr.cumulative_state == self.CUMULATIVE_STATE_NO: - raise Http400('Cumulation cannot be closed for non-cumulative dataset') + raise Http400("Cumulation cannot be closed for non-cumulative dataset") elif self.cumulative_state == self.CUMULATIVE_STATE_CLOSED: - _logger.info('Note: cumulative_state is already CLOSED. Doing nothing') + _logger.info("Note: cumulative_state is already CLOSED. Doing nothing") return self.date_cumulation_ended = self.date_modified @@ -1423,18 +1528,18 @@ def change_cumulative_state(self, new_state): if comparison_cr.preservation_state > self.PRESERVATION_STATE_INITIALIZED: raise Http400( - 'Cumulative datasets are not allowed in PAS process. Change preservation_state ' - 'to 0 in order to change the dataset to cumulative.' + "Cumulative datasets are not allowed in PAS process. Change preservation_state " + "to 0 in order to change the dataset to cumulative." ) elif comparison_cr.files.count() > 0: # permits opening cumulativity for a published dataset that does not yet # have any files. raise Http400( - 'Can\'t set dataset cumulative: Dataset already has files. Please create ' - 'a new dataset version first.' + "Can't set dataset cumulative: Dataset already has files. Please create " + "a new dataset version first." ) elif self.cumulative_state == self.CUMULATIVE_STATE_YES: - _logger.info('Note: cumulative_state is already YES. Doing nothing') + _logger.info("Note: cumulative_state is already YES. Doing nothing") return self.date_cumulation_started = self.date_modified @@ -1447,7 +1552,7 @@ def change_cumulative_state(self, new_state): super(CatalogRecord, self).save() # Handles with drafts - self.add_post_request_callable(RabbitMQPublishRecord(self, 'update')) + self.add_post_request_callable(RabbitMQPublishRecord(self, "update")) def calculate_directory_byte_sizes_and_file_counts(self): """ @@ -1456,7 +1561,7 @@ def calculate_directory_byte_sizes_and_file_counts(self): if not self.files.exists(): return - _logger.info('Calculating directory byte_sizes and file_counts...') + _logger.info("Calculating directory byte_sizes and file_counts...") parent_dir = self.files.first().parent_directory @@ -1470,4 +1575,4 @@ def calculate_directory_byte_sizes_and_file_counts(self): root_dir.calculate_byte_size_and_file_count_for_cr(self.id, directory_data) self._directory_data = directory_data - super(Common, self).save(update_fields=['_directory_data']) + super(Common, self).save(update_fields=["_directory_data"]) diff --git a/src/metax_api/models/common.py b/src/metax_api/models/common.py index b448a1b1..62b5f0b6 100755 --- a/src/metax_api/models/common.py +++ b/src/metax_api/models/common.py @@ -15,7 +15,6 @@ class CommonManager(models.Manager): - def get_queryset(self): return super(CommonManager, self).get_queryset().filter(active=True, removed=False) @@ -31,10 +30,16 @@ class Common(models.Model): user_modified = models.CharField(max_length=200, null=True) date_created = models.DateTimeField() user_created = models.CharField(max_length=200, null=True) - service_modified = models.CharField(max_length=200, null=True, - help_text='Name of the service who last modified the record') - service_created = models.CharField(max_length=200, null=True, - help_text='Name of the service who created the record') + service_modified = models.CharField( + max_length=200, + null=True, + help_text="Name of the service who last modified the record", + ) + service_created = models.CharField( + max_length=200, + null=True, + help_text="Name of the service who created the record", + ) date_removed = models.DateTimeField(null=True) # END OF MODEL FIELD DEFINITIONS # @@ -50,14 +55,14 @@ class Common(models.Model): class Meta: indexes = [ - models.Index(fields=['active']), - models.Index(fields=['removed']), + models.Index(fields=["active"]), + models.Index(fields=["removed"]), ] abstract = True def __init__(self, *args, **kwargs): - if '__request' in kwargs: - self.request = kwargs.pop('__request') + if "__request" in kwargs: + self.request = kwargs.pop("__request") super(Common, self).__init__(*args, **kwargs) @@ -65,9 +70,9 @@ def __init__(self, *args, **kwargs): self._tracked_fields = [] self.track_fields( - 'date_created', - 'user_created', - 'service_created', + "date_created", + "user_created", + "service_created", ) def save(self, *args, **kwargs): @@ -83,7 +88,7 @@ def force_save(self, *args, **kwargs): checks. Should be used only in testing to set up data for a test case. """ if not executing_test_case(): - raise Exception('this method should only be used inside a test case') + raise Exception("this method should only be used inside a test case") super(Common, self).save(*args, **kwargs) self._update_tracked_field_values() @@ -92,7 +97,7 @@ def remove(self): Mark record as removed, never delete from db. """ self._set_removed() - super().save(update_fields=['removed', 'date_removed', 'date_modified']) + super().save(update_fields=["removed", "date_removed", "date_modified"]) self._update_tracked_field_values() def user_has_access(self, request): @@ -140,7 +145,7 @@ def track_fields(self, *fields): self._tracked_fields.append(field_name) - if '.' in field_name: + if "." in field_name: self._track_json_field(field_name) else: if self._field_is_loaded(field_name): @@ -160,7 +165,7 @@ def _unset_removed(self): self.date_modified = get_tz_aware_now_without_micros() def _track_json_field(self, field_name): - field_name, json_field_name = field_name.split('.') + field_name, json_field_name = field_name.split(".") if self._field_is_loaded(field_name) and json_field_name in getattr(self, field_name): json_field_value = getattr(self, field_name)[json_field_name] @@ -194,7 +199,7 @@ def field_changed(self, field_name): """ Check if a tracked field has changed since last saved to db. """ - if '.' in field_name: + if "." in field_name: return self._json_field_changed(field_name) if not self._field_is_loaded(field_name): @@ -203,13 +208,13 @@ def field_changed(self, field_name): if self._field_is_tracked(field_name): # pragma: no cover if not self._field_initial_value_loaded(field_name): self._raise_field_not_tracked_error(field_name) - else: # pragma: no cover - raise FieldError('Field %s is not being tracked for changes' % (field_name)) + else: # pragma: no cover + raise FieldError("Field %s is not being tracked for changes" % (field_name)) return getattr(self, field_name) != self._initial_data[field_name] def _json_field_changed(self, field_name_full): - field_name, json_field_name = field_name_full.split('.') + field_name, json_field_name = field_name_full.split(".") if not self._field_is_loaded(field_name): return False @@ -217,8 +222,8 @@ def _json_field_changed(self, field_name_full): if self._field_is_tracked(field_name_full): # pragma: no cover if not self._field_initial_value_loaded(field_name): self._raise_field_not_tracked_error(field_name_full) - else: # pragma: no cover - raise FieldError('Field %s is not being tracked for changes' % (field_name_full)) + else: # pragma: no cover + raise FieldError("Field %s is not being tracked for changes" % (field_name_full)) json_field_value = self._initial_data[field_name].get(json_field_name, None) return getattr(self, field_name).get(json_field_name, None) != json_field_value @@ -238,18 +243,18 @@ def _raise_field_not_tracked_error(self, field_name): their initial query to include all the data they were going to need anyway. """ raise FieldError( - 'Tried to check changes in field %(field_name)s, but the field was not loaded for ' - 'tracking changes during __init__. Call .only(%(field_name)s) in you ORM query to ' - 'load the field during __init__, so that it will be tracked.' % locals() + "Tried to check changes in field %(field_name)s, but the field was not loaded for " + "tracking changes during __init__. Call .only(%(field_name)s) in you ORM query to " + "load the field during __init__, so that it will be tracked." % locals() ) def _check_read_only_after_create_fields(self): - if self.field_changed('date_created'): - self.date_created = self._initial_data['date_created'] - if self.field_changed('user_created'): - self.user_created = self._initial_data['user_created'] - if self.field_changed('service_created'): - self.service_created = self._initial_data['service_created'] + if self.field_changed("date_created"): + self.date_created = self._initial_data["date_created"] + if self.field_changed("user_created"): + self.user_created = self._initial_data["user_created"] + if self.field_changed("service_created"): + self.service_created = self._initial_data["service_created"] def _operation_is_create(self): return self.id is None @@ -263,10 +268,12 @@ def _update_tracked_field_values(self): field_changed() keeps working as expected """ for field_name in self._initial_data.keys(): - if '.' in field_name: - field_name, json_field_name = field_name.split('.') + if "." in field_name: + field_name, json_field_name = field_name.split(".") # by now should have crashed to checks in previous steps, so no need to check here - self._initial_data[field_name][json_field_name] = getattr(self, field_name).get(json_field_name, None) + self._initial_data[field_name][json_field_name] = getattr(self, field_name).get( + json_field_name, None + ) else: self._initial_data[field_name] = getattr(self, field_name) diff --git a/src/metax_api/models/contract.py b/src/metax_api/models/contract.py index c5940eb3..e7b75030 100755 --- a/src/metax_api/models/contract.py +++ b/src/metax_api/models/contract.py @@ -26,18 +26,19 @@ def delete(self): https://docs.djangoproject.com/en/1.11/topics/db/models/#overriding-model-methods """ super(Contract, self).remove() - sql = 'update metax_api_catalogrecord set removed = true ' \ - 'where active = true and removed = false ' \ - 'and contract_id = %s' + sql = ( + "update metax_api_catalogrecord set removed = true " + "where active = true and removed = false " + "and contract_id = %s" + ) with connection.cursor() as cr: cr.execute(sql, [self.id]) def __repr__(self): - return '<%s: %d, removed: %s, identifier: %d, record_count: %d >' \ - % ( - 'Contract', - self.id, - str(self.removed), - self.contract_json['identifier'], - self.records.count(), - ) + return "<%s: %d, removed: %s, identifier: %d, record_count: %d >" % ( + "Contract", + self.id, + str(self.removed), + self.contract_json["identifier"], + self.records.count(), + ) diff --git a/src/metax_api/models/data_catalog.py b/src/metax_api/models/data_catalog.py index f2660a04..37e2c100 100755 --- a/src/metax_api/models/data_catalog.py +++ b/src/metax_api/models/data_catalog.py @@ -18,57 +18,75 @@ class DataCatalog(Common): catalog_json = JSONField() catalog_record_group_edit = models.CharField( - max_length=200, blank=False, null=True, - help_text='Group which is allowed to edit catalog records in the catalog.') + max_length=200, + blank=False, + null=True, + help_text="Group which is allowed to edit catalog records in the catalog.", + ) catalog_record_group_create = models.CharField( - max_length=200, blank=False, null=True, - help_text='Group which is allowed to add new catalog records to the catalog.') + max_length=200, + blank=False, + null=True, + help_text="Group which is allowed to add new catalog records to the catalog.", + ) catalog_record_services_edit = models.CharField( - max_length=200, blank=False, null=True, - help_text='Services which are allowed to edit catalog records in the catalog.') + max_length=200, + blank=False, + null=True, + help_text="Services which are allowed to edit catalog records in the catalog.", + ) catalog_record_services_create = models.CharField( - max_length=200, blank=False, null=True, - help_text='Services which are allowed to edit catalog records in the catalog.') + max_length=200, + blank=False, + null=True, + help_text="Services which are allowed to edit catalog records in the catalog.", + ) catalog_record_group_read = models.CharField( - max_length=200, blank=False, null=True, - help_text='Group which is allowed to read catalog records in the catalog.') + max_length=200, + blank=False, + null=True, + help_text="Group which is allowed to read catalog records in the catalog.", + ) catalog_record_services_read = models.CharField( - max_length=200, blank=False, null=True, - help_text='Services which are allowed to read catalog records in the catalog.') + max_length=200, + blank=False, + null=True, + help_text="Services which are allowed to read catalog records in the catalog.", + ) # END OF MODEL FIELD DEFINITIONS # - READ_METHODS = ('GET', 'HEAD', 'OPTIONS') + READ_METHODS = ("GET", "HEAD", "OPTIONS") def __init__(self, *args, **kwargs): super(DataCatalog, self).__init__(*args, **kwargs) - self.track_fields('catalog_json.identifier') + self.track_fields("catalog_json.identifier") def save(self, *args, **kwargs): if self._operation_is_update(): - if self.field_changed('catalog_json.identifier'): + if self.field_changed("catalog_json.identifier"): # read-only after creating - self.catalog_json['identifier'] = self._initial_data['catalog_json']['identifier'] + self.catalog_json["identifier"] = self._initial_data["catalog_json"]["identifier"] super(DataCatalog, self).save(*args, **kwargs) - def print_records(self): # pragma: no cover + def print_records(self): # pragma: no cover for r in self.records.all(): print(r) def __repr__(self): - return '<%s: %d, removed: %s, identifier: %s, research_dataset_schema=%s, dataset_versioning: %s >' % ( - 'DataCatalog', + return "<%s: %d, removed: %s, identifier: %s, research_dataset_schema=%s, dataset_versioning: %s >" % ( + "DataCatalog", self.id, str(self.removed), - self.catalog_json['identifier'], - self.catalog_json['research_dataset_schema'], - self.catalog_json['dataset_versioning'], + self.catalog_json["identifier"], + self.catalog_json["research_dataset_schema"], + self.catalog_json["dataset_versioning"], ) def user_has_access(self, request): @@ -81,4 +99,4 @@ def user_has_access(self, request): return False def delete(self): - super(DataCatalog, self).remove() \ No newline at end of file + super(DataCatalog, self).remove() diff --git a/src/metax_api/models/deleted_object.py b/src/metax_api/models/deleted_object.py new file mode 100644 index 00000000..f73f38c5 --- /dev/null +++ b/src/metax_api/models/deleted_object.py @@ -0,0 +1,18 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT + +from django.db import models +from django.db.models import JSONField + +from metax_api.utils import get_tz_aware_now_without_micros + + +class DeletedObject(models.Model): + + model_name = models.CharField(max_length=200) + object_data = JSONField(null=False) + date_deleted = models.DateTimeField(default=get_tz_aware_now_without_micros) \ No newline at end of file diff --git a/src/metax_api/models/directory.py b/src/metax_api/models/directory.py index 20bfa264..5b8c1714 100755 --- a/src/metax_api/models/directory.py +++ b/src/metax_api/models/directory.py @@ -27,17 +27,19 @@ class Directory(Common): directory_path = models.TextField() identifier = models.CharField(max_length=200, unique=True) file_count = models.BigIntegerField(default=0) - parent_directory = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, related_name='child_directories') + parent_directory = models.ForeignKey( + "self", on_delete=models.SET_NULL, null=True, related_name="child_directories" + ) project_identifier = models.CharField(max_length=200) # END OF MODEL FIELD DEFINITIONS # class Meta: indexes = [ - models.Index(fields=['directory_path']), - models.Index(fields=['identifier']), - models.Index(fields=['parent_directory']), - models.Index(fields=['project_identifier']), + models.Index(fields=["directory_path"]), + models.Index(fields=["identifier"]), + models.Index(fields=["parent_directory"]), + models.Index(fields=["project_identifier"]), ] def delete(self): @@ -48,6 +50,7 @@ def user_has_access(self, request): if request.user.is_service: return True from metax_api.services import AuthService + return self.project_identifier in AuthService.get_user_projects(request) def calculate_byte_size_and_file_count(self): @@ -58,17 +61,20 @@ def calculate_byte_size_and_file_count(self): """ if self.parent_directory_id: raise Exception( - 'while this is a recursive method, it is intended to be initially called by ' - 'project root directories only.' + "while this is a recursive method, it is intended to be initially called by " + "project root directories only." ) - _logger.info('Calculating directory byte sizes and file counts for project %s...' % self.project_identifier) + _logger.info( + "Calculating directory byte sizes and file counts for project %s..." + % self.project_identifier + ) update_statements = [] self._calculate_byte_size_and_file_count(update_statements) - sql_update_all_directories = ''' + sql_update_all_directories = """ update metax_api_directory as d set byte_size = results.byte_size, file_count = results.file_count @@ -76,19 +82,21 @@ def calculate_byte_size_and_file_count(self): %s ) as results(byte_size, file_count, id) where results.id = d.id; - ''' % ','.join(update_statements) + """ % ",".join( + update_statements + ) with connection.cursor() as cursor: cursor.execute(sql_update_all_directories) _logger.info( - 'Project %s directory tree calculations complete. Total byte_size: ' - '%d bytes (%.3f GB), total file_count: %d files' + "Project %s directory tree calculations complete. Total byte_size: " + "%d bytes (%.3f GB), total file_count: %d files" % ( self.project_identifier, self.byte_size, self.byte_size / 1024 / 1024 / 1024, - self.file_count + self.file_count, ) ) @@ -101,10 +109,16 @@ def _calculate_byte_size_and_file_count(self, update_statements): self.file_count = 0 # fields id, parent_directory_id must be specified for joining for Prefetch-object to work properly - sub_dirs = self.child_directories.all() \ - .only('byte_size', 'parent_directory_id') \ + sub_dirs = ( + self.child_directories.all() + .only("byte_size", "parent_directory_id") .prefetch_related( - Prefetch('files', queryset=File.objects.only('id', 'byte_size', 'parent_directory_id'))) + Prefetch( + "files", + queryset=File.objects.only("id", "byte_size", "parent_directory_id"), + ) + ) + ) if sub_dirs: for sub_dir in sub_dirs: @@ -118,10 +132,7 @@ def _calculate_byte_size_and_file_count(self, update_statements): self.byte_size += sum(f.byte_size for f in self.files.all()) or 0 self.file_count += len(self.files.all()) or 0 - update_statements.append( - '(%d, %d, %d)' - % (self.byte_size, self.file_count, self.id) - ) + update_statements.append("(%d, %d, %d)" % (self.byte_size, self.file_count, self.id)) def calculate_byte_size_and_file_count_for_cr(self, cr_id, directory_data): """ @@ -130,13 +141,20 @@ def calculate_byte_size_and_file_count_for_cr(self, cr_id, directory_data): file count for each directory into parameter directory_data. Intended to be called for the top-level directories of a project in a dataset. """ - _logger.debug('Calculating directory byte sizes and file counts for project %s, directory %s...' % - (self.project_identifier, self.directory_path)) + _logger.debug( + "Calculating directory byte sizes and file counts for project %s, directory %s..." + % (self.project_identifier, self.directory_path) + ) - stats = File.objects.filter(record__pk=cr_id).values_list('parent_directory_id').annotate( - Sum('byte_size'), Count('id')) + stats = ( + File.objects.filter(record__pk=cr_id) + .values_list("parent_directory_id") + .annotate(Sum("byte_size"), Count("id")) + ) - grouped_by_dir = {parent_id: (byte_size, file_count) for parent_id, byte_size, file_count in stats} + grouped_by_dir = { + parent_id: (byte_size, file_count) for parent_id, byte_size, file_count in stats + } self._calculate_byte_size_and_file_count_for_cr(grouped_by_dir, directory_data) @@ -155,7 +173,7 @@ def _calculate_byte_size_and_file_count_for_cr(self, grouped_by_dir, directory_d self.byte_size = 0 self.file_count = 0 - sub_dirs = self.child_directories.all().only('id') + sub_dirs = self.child_directories.all().only("id") if sub_dirs: for sub_dir in sub_dirs: @@ -172,14 +190,17 @@ def _calculate_byte_size_and_file_count_for_cr(self, grouped_by_dir, directory_d self.file_count += current_dir[1] # the accumulated numbers that exist in the directory for given cr - directory_data[self.id] = [ self.byte_size, self.file_count ] + directory_data[self.id] = [self.byte_size, self.file_count] def __repr__(self): - return '<%s: %d, removed: %s, project_identifier: %s, identifier: %s, directory_path: %s >' % ( - 'Directory', - self.id, - str(self.removed), - self.project_identifier, - self.identifier, - self.directory_path + return ( + "<%s: %d, removed: %s, project_identifier: %s, identifier: %s, directory_path: %s >" + % ( + "Directory", + self.id, + str(self.removed), + self.project_identifier, + self.identifier, + self.directory_path, + ) ) diff --git a/src/metax_api/models/file.py b/src/metax_api/models/file.py index add9fa72..9cbad0c6 100755 --- a/src/metax_api/models/file.py +++ b/src/metax_api/models/file.py @@ -13,22 +13,24 @@ class FileManager(CommonManager): - def get(self, *args, **kwargs): - if kwargs.get('using_dict', None): + if kwargs.get("using_dict", None): # for a simple "just get me the instance that equals this dict i have" search. # this is useful if during a request the url does not contain the identifier (bulk update), # and in generic operations where the type of object being handled is not known (also bulk operations). - row = kwargs.pop('using_dict') - if row.get('id', None): - kwargs['id'] = row['id'] - elif row.get('identifier', None): - kwargs['identifier'] = row['identifier'] + row = kwargs.pop("using_dict") + if row.get("id", None): + kwargs["id"] = row["id"] + elif row.get("identifier", None): + kwargs["identifier"] = row["identifier"] else: - raise ValidationError([ - 'this operation requires one of the following identifying keys to be present: %s' - % ', '.join([ 'id', 'identifier' ])]) + raise ValidationError( + [ + "this operation requires one of the following identifying keys to be present: %s" + % ", ".join(["id", "identifier"]) + ] + ) return super(FileManager, self).get(*args, **kwargs) @@ -48,22 +50,26 @@ class File(Common): file_modified = models.DateTimeField() file_name = models.TextField() file_path = models.TextField() - file_storage = models.ForeignKey('metax_api.FileStorage', on_delete=models.DO_NOTHING) + file_storage = models.ForeignKey("metax_api.FileStorage", on_delete=models.DO_NOTHING) file_uploaded = models.DateTimeField() identifier = models.CharField(max_length=200) open_access = models.BooleanField(default=False) - parent_directory = models.ForeignKey('metax_api.Directory', on_delete=models.SET_NULL, null=True, - related_name='files') + parent_directory = models.ForeignKey( + "metax_api.Directory", + on_delete=models.SET_NULL, + null=True, + related_name="files", + ) project_identifier = models.CharField(max_length=200) # END OF MODEL FIELD DEFINITIONS # class Meta: indexes = [ - models.Index(fields=['file_path']), - models.Index(fields=['identifier']), - models.Index(fields=['parent_directory']), - models.Index(fields=['project_identifier']), + models.Index(fields=["file_path"]), + models.Index(fields=["identifier"]), + models.Index(fields=["parent_directory"]), + models.Index(fields=["project_identifier"]), ] objects = FileManager() @@ -72,17 +78,21 @@ def user_has_access(self, request): if request.user.is_service: return True from metax_api.services import AuthService + return self.project_identifier in AuthService.get_user_projects(request) def __repr__(self): - return '<%s: %d, removed: %s, project_identifier: %s, identifier: %s, file_path: %s >' % ( - 'File', + return "<%s: %d, removed: %s, project_identifier: %s, identifier: %s, file_path: %s >" % ( + "File", self.id, str(self.removed), self.project_identifier, self.identifier, - self.file_path + self.file_path, ) - def delete(self): - super(File, self).remove() \ No newline at end of file + def delete(self, *args, **kwargs): + if kwargs.get("hard"): + super().delete() + else: + super(File, self).remove() diff --git a/src/metax_api/models/file_storage.py b/src/metax_api/models/file_storage.py index 1a1d5287..fbe89086 100755 --- a/src/metax_api/models/file_storage.py +++ b/src/metax_api/models/file_storage.py @@ -20,23 +20,25 @@ class FileStorage(Common): def __init__(self, *args, **kwargs): super(FileStorage, self).__init__(*args, **kwargs) - self.track_fields('file_storage_json.identifier') + self.track_fields("file_storage_json.identifier") def save(self, *args, **kwargs): if self._operation_is_update(): - if self.field_changed('file_storage_json.identifier'): + if self.field_changed("file_storage_json.identifier"): # read-only after creating - self.file_storage_json['identifier'] = self._initial_data['file_storage_json']['identifier'] + self.file_storage_json["identifier"] = self._initial_data["file_storage_json"][ + "identifier" + ] super(FileStorage, self).save(*args, **kwargs) def __repr__(self): - return '<%s: %d, removed: %s, identifier: %s >' % ( - 'FileStorage', + return "<%s: %d, removed: %s, identifier: %s >" % ( + "FileStorage", self.id, str(self.removed), - self.file_storage_json['identifier'], + self.file_storage_json["identifier"], ) def delete(self): - super(FileStorage, self).remove() \ No newline at end of file + super(FileStorage, self).remove() diff --git a/src/metax_api/models/metax_user.py b/src/metax_api/models/metax_user.py index a83b7072..7d8bd10b 100755 --- a/src/metax_api/models/metax_user.py +++ b/src/metax_api/models/metax_user.py @@ -14,5 +14,6 @@ https://docs.djangoproject.com/en/1.11/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project """ + class MetaxUser(AbstractUser): pass diff --git a/src/metax_api/models/xml_metadata.py b/src/metax_api/models/xml_metadata.py index 24572735..5d180933 100755 --- a/src/metax_api/models/xml_metadata.py +++ b/src/metax_api/models/xml_metadata.py @@ -22,10 +22,10 @@ class XmlMetadata(Common): # END OF MODEL FIELD DEFINITIONS # class Meta: - unique_together = ('namespace', 'file') + unique_together = ("namespace", "file") indexes = [ - models.Index(fields=['namespace']), + models.Index(fields=["namespace"]), ] def delete(self): diff --git a/src/metax_api/onappstart.py b/src/metax_api/onappstart.py index 24f85914..2a0346c6 100755 --- a/src/metax_api/onappstart.py +++ b/src/metax_api/onappstart.py @@ -13,7 +13,7 @@ from django.conf import settings from icecream import ic -from metax_api.utils import ReferenceDataLoader, executing_test_case +from metax_api.utils import ReferenceDataLoader, executing_test_case, convert_yaml_to_html _logger = logging.getLogger(__name__) @@ -40,6 +40,7 @@ def ready(self): # pragma: no cover # because the "django apps" have not been loaded yet. import json + import metax_api.signals # noqa from metax_api.services import RabbitMQService as rabbitmq from metax_api.services.redis_cache_service import RedisClient @@ -101,4 +102,10 @@ def ready(self): # pragma: no cover _logger.error(e) _logger.error("Unable to initialize RabbitMQ exchanges") + try: + convert_yaml_to_html.yaml_to_html_convert() + except Exception as e: + _logger.error(e) + _logger.error("Unable to convert swagger documentation") + _logger.info("Metax API startup tasks finished") diff --git a/src/metax_api/parsers/parsers.py b/src/metax_api/parsers/parsers.py index 9ab9fa9e..9ca82c33 100755 --- a/src/metax_api/parsers/parsers.py +++ b/src/metax_api/parsers/parsers.py @@ -13,9 +13,10 @@ lets it pass through to the views in its original form. """ + class XMLParser(parsers.BaseParser): - media_type = 'application/xml' + media_type = "application/xml" def parse(self, stream, media_type=None, parser_context=None): - return stream.read().decode('utf-8') + return stream.read().decode("utf-8") diff --git a/src/metax_api/permissions/permissions.py b/src/metax_api/permissions/permissions.py index 935e2d90..93865817 100755 --- a/src/metax_api/permissions/permissions.py +++ b/src/metax_api/permissions/permissions.py @@ -16,13 +16,13 @@ _logger = logging.getLogger(__name__) METHOD_MAP = { - 'GET': 'read', - 'HEAD': 'read', - 'OPTIONS': 'read', - 'POST': 'create', - 'PUT': 'update', - 'PATCH': 'update', - 'DELETE': 'delete', + "GET": "read", + "HEAD": "read", + "OPTIONS": "read", + "POST": "create", + "PUT": "update", + "PATCH": "update", + "DELETE": "delete", } @@ -76,32 +76,31 @@ def has_permission(self, request, view): api_name = view.get_api_name() has_perm = False - _logger.debug('Checking user permission for api %s...' % request.path) + _logger.debug("Checking user permission for api %s..." % request.path) - if api_type not in self.perms: # pragma: no cover + if api_type not in self.perms: # pragma: no cover _logger.error( - 'api_type %s not specified in self.perms - forbidding. this probably should not happen' + "api_type %s not specified in self.perms - forbidding. this probably should not happen" % api_type ) has_perm = False - elif api_name not in self.perms[api_type]: # pragma: no cover + elif api_name not in self.perms[api_type]: # pragma: no cover _logger.error( - 'api_name %s not specified in self.perms[\'%s\'] - forbidding. this probably should not happen' % - (api_name, api_type) + "api_name %s not specified in self.perms['%s'] - forbidding. this probably should not happen" + % (api_name, api_type) ) has_perm = False - elif api_type == 'rest': + elif api_type == "rest": has_perm = self._check_rest_perms(request, api_name) - elif api_type == 'rpc': + elif api_type == "rpc": has_perm = self._check_rpc_perms(request, api_name) else: - _logger.error('Unknown api %s' % request.path) + _logger.error("Unknown api %s" % request.path) raise Exception("request path not available") - _logger.debug( - 'user %s has_perm for api %s == %r' - % (request.user.username or '(anonymous)', request.path, has_perm) + "user %s has_perm for api %s == %r" + % (request.user.username or "(anonymous)", request.path, has_perm) ) return has_perm @@ -113,12 +112,14 @@ def _check_rest_perms(self, request, api_name): """ if request.method in METHOD_MAP: operation_type = METHOD_MAP[request.method] - if 'all' in self.perms['rest'][api_name].get(operation_type, []): + if "all" in self.perms["rest"][api_name].get(operation_type, []): has_perm = True else: has_perm = self._check_user_rest_perms(request, api_name, operation_type) if not has_perm: - _logger.error(f"access denied for user {request.user.username} in {api_name} with operation {operation_type}") + _logger.error( + f"access denied for user {request.user.username} in {api_name} with operation {operation_type}" + ) else: raise MethodNotAllowed return has_perm @@ -128,16 +129,22 @@ def _check_rpc_perms(self, request, api_name): Check if user (service user) or user type (endusers) has permission to use given RPC method. """ - rpc_method_name = request.path.split('/')[-1] - - if rpc_method_name not in self.perms['rpc'][api_name]: - raise Http400({ - 'detail': [ - 'Unknown RPC method: %s. Valid %s RPC methods are: %s' - % (rpc_method_name, api_name, ', '.join(self.perms['rpc'][api_name].keys())) - ] - }) - elif 'all' in self.perms['rpc'][api_name][rpc_method_name]['use']: + rpc_method_name = request.path.split("/")[-1] + + if rpc_method_name not in self.perms["rpc"][api_name]: + raise Http400( + { + "detail": [ + "Unknown RPC method: %s. Valid %s RPC methods are: %s" + % ( + rpc_method_name, + api_name, + ", ".join(self.perms["rpc"][api_name].keys()), + ) + ] + } + ) + elif "all" in self.perms["rpc"][api_name][rpc_method_name]["use"]: has_perm = True else: has_perm = self._check_user_rpc_perms(request, api_name, rpc_method_name) @@ -147,7 +154,7 @@ def _check_rpc_perms(self, request, api_name): def has_object_permission(self, request, view, obj): has_perm = obj.user_has_access(request) if not has_perm: - self.message = 'You are not permitted to access this resource.' + self.message = "You are not permitted to access this resource." return has_perm @@ -158,13 +165,13 @@ class EndUserPermissions(MetaxAPIPermissions): """ service_permission = False - message = 'End Users are not allowed to access this api.' + message = "End Users are not allowed to access this api." def _check_user_rest_perms(self, request, api_name, operation_type): - return 'endusers' in self.perms['rest'][api_name].get(operation_type, []) + return "endusers" in self.perms["rest"][api_name].get(operation_type, []) def _check_user_rpc_perms(self, request, api_name, rpc_method_name): - return 'endusers' in self.perms['rpc'][api_name][rpc_method_name]['use'] + return "endusers" in self.perms["rpc"][api_name][rpc_method_name]["use"] class ServicePermissions(MetaxAPIPermissions): @@ -175,7 +182,7 @@ class ServicePermissions(MetaxAPIPermissions): """ service_permission = True - message = 'Service %s is not allowed to access this api.' + message = "Service %s is not allowed to access this api." def has_permission(self, request, view): has_perm = super().has_permission(request, view) @@ -185,7 +192,7 @@ def has_permission(self, request, view): return has_perm def _check_user_rest_perms(self, request, api_name, operation_type): - return request.user.username in self.perms['rest'][api_name].get(operation_type, []) + return request.user.username in self.perms["rest"][api_name].get(operation_type, []) def _check_user_rpc_perms(self, request, api_name, rpc_method_name): - return request.user.username in self.perms['rpc'][api_name][rpc_method_name]['use'] + return request.user.username in self.perms["rpc"][api_name][rpc_method_name]["use"] diff --git a/src/metax_api/renderers/renderers.py b/src/metax_api/renderers/renderers.py index 440c12d7..862bb054 100755 --- a/src/metax_api/renderers/renderers.py +++ b/src/metax_api/renderers/renderers.py @@ -18,8 +18,13 @@ class HTMLToJSONRenderer(renderers.JSONRenderer): This renderer catches the 'text/html' Accept header, but returns JSON instead of html. """ - media_type = 'text/html' - charset = 'utf-8' + media_type = "text/html" + charset = "utf-8" + + def render(self, data, media_type=None, renderer_context=None): + rendered_data = super().render(data, media_type, renderer_context) + renderer_context['response']['Content-Type'] = "application/json; charset=utf-8" + return rendered_data class XMLRenderer(renderers.BaseRenderer): @@ -29,8 +34,8 @@ class XMLRenderer(renderers.BaseRenderer): just isnt complicated enough in django's opinion. """ - media_type = 'application/xml' - format = 'xml' + media_type = "application/xml" + format = "xml" def render(self, data, media_type=None, renderer_context=None): return data diff --git a/src/metax_api/services/__init__.py b/src/metax_api/services/__init__.py index f699acc0..63bc45af 100755 --- a/src/metax_api/services/__init__.py +++ b/src/metax_api/services/__init__.py @@ -5,7 +5,6 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from .api_error_service import ApiErrorService from .auth_service import AuthService from .callable_service import CallableService from .catalog_record_service import CatalogRecordService diff --git a/src/metax_api/services/api_error_service.py b/src/metax_api/services/api_error_service.py deleted file mode 100755 index 39952b23..00000000 --- a/src/metax_api/services/api_error_service.py +++ /dev/null @@ -1,135 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -import logging -import traceback -from json import dump as json_dump, load as json_load -from os import listdir, remove as remove_file -from uuid import uuid4 - -from django.conf import settings - -from metax_api.utils import executing_test_case, get_tz_aware_now_without_micros, json_logger - -_logger = logging.getLogger(__name__) - - -class ApiErrorService(): - - @staticmethod - def flush_errors(): - """ - Delete all error files. - """ - error_files = listdir(settings.ERROR_FILES_PATH) - file_count = len(error_files) - for ef in error_files: - remove_file('%s/%s' % (settings.ERROR_FILES_PATH, ef)) - return file_count - - @staticmethod - def remove_error_file(error_identifier): - """ - Delete a single error file. - """ - remove_file('%s/%s.json' % (settings.ERROR_FILES_PATH, error_identifier)) - - @staticmethod - def retrieve_error_details(error_identifier): - """ - Retrieve complete data about a single error - """ - with open('%s/%s.json' % (settings.ERROR_FILES_PATH, error_identifier), 'r') as f: - return json_load(f) - - @staticmethod - def retrieve_error_list(): - """ - List all error files in the designated error location. Data is cleaned up a bit - for easier browsing. - """ - error_files = listdir(settings.ERROR_FILES_PATH) - error_list = [] - - for ef in error_files: - with open('%s/%s' % (settings.ERROR_FILES_PATH, ef), 'r') as f: - error_details = json_load(f) - error_details.pop('data', None) - error_details.pop('headers', None) - if len(str(error_details['response'])) > 200: - error_details['response'] = '%s ...(first 200 characters)' % str(error_details['response'])[:200] - error_details['traceback'] = '(last 200 characters) ...%s' % error_details['traceback'][-200:] - error_list.append(error_details) - return error_list - - @staticmethod - def store_error_details(request, response, exception=None, other={}): - """ - Store error and request details to disk to specified error file location. - """ - current_time = str(get_tz_aware_now_without_micros()).replace(' ', 'T') - - if request.method in ('POST', 'PUT', 'PATCH'): - # cast possible datetime objects to strings, because those cant be json-serialized... - request_data = request.data - for date_field in ('date_modified', 'date_created'): - if isinstance(request_data, list): - for item in request_data: - if isinstance(item, dict) and date_field in item: - item[date_field] = str(item[date_field]) - elif isinstance(request_data, dict) and date_field in request_data: - request_data[date_field] = str(request_data[date_field]) - else: - pass - else: - request_data = None - - error_info = { - 'method': request.method, - 'user': request.user.username or 'guest', - 'data': request_data, - 'headers': { - k: v for k, v in request.META.items() - if k.startswith('HTTP_') and k != 'HTTP_AUTHORIZATION' - }, - 'status_code': response.status_code, - 'response': response.data, - 'traceback': traceback.format_exc(), - # during test case execution, RAW_URI is not set - 'url': request.META.get('RAW_URI', request.META.get('PATH_INFO', '???')), - 'identifier': '%s-%s' % (current_time[:19], str(uuid4())[:8]), - 'exception_time': current_time, - } - - if other: - # may contain info that the request was a bulk operation - error_info['other'] = { k: v for k, v in other.items() } - if 'bulk_request' in other: - error_info['other']['data_row_count'] = len(request_data) - - try: - with open('%s/%s.json' % (settings.ERROR_FILES_PATH, error_info['identifier']), 'w') as f: - json_dump(error_info, f) - except: - _logger.exception('Failed to save error info...') - else: - - response.data['error_identifier'] = error_info['identifier'] - - if response.status_code == 500: - - json_logger.error( - event='api_exception', - error={ - 'error_identifier': error_info['identifier'], - 'status_code': response.status_code, - 'traceback': error_info['traceback'], - } - ) - - if executing_test_case(): - response.data['traceback'] = traceback.format_exc() diff --git a/src/metax_api/services/auth_service.py b/src/metax_api/services/auth_service.py index 795f1c48..bb2fee69 100755 --- a/src/metax_api/services/auth_service.py +++ b/src/metax_api/services/auth_service.py @@ -13,8 +13,8 @@ _logger = logging.getLogger(__name__) -class AuthService(): +class AuthService: @classmethod def get_user_projects(cls, request): """ @@ -26,12 +26,12 @@ def get_user_projects(cls, request): # in order to not leak the user any information. raise Http404 - if hasattr(request.user, 'user_projects'): + if hasattr(request.user, "user_projects"): return request.user.user_projects user_projects = cls.extract_file_projects_from_token(request.user.token) - username = request.user.token.get('CSCUserName', '') + username = request.user.token.get("CSCUserName", "") additional_projects = cls.get_additional_user_projects_from_file(username) user_projects.update(additional_projects) @@ -40,7 +40,7 @@ def get_user_projects(cls, request): @staticmethod def extract_file_projects_from_token(token): - ''' + """ Extract user's project identifiers from token claims. The user's token (request.user.token), when the user has access to some @@ -55,17 +55,17 @@ def extract_file_projects_from_token(token): ] Valid group names for IDA projects look like: IDA01:2001036. - ''' + """ if not token: return set() user_projects = set() - project_prefix = 'IDA01:' + project_prefix = "IDA01:" user_projects = set( - group.split(':')[-1] - for group in token.get('group_names', []) + group.split(":")[-1] + for group in token.get("group_names", []) if group.startswith(project_prefix) ) @@ -81,9 +81,9 @@ def get_additional_user_projects_from_file(username): additional_projects = None try: - with open(settings.ADDITIONAL_USER_PROJECTS_PATH, 'r') as file: + with open(settings.ADDITIONAL_USER_PROJECTS_PATH, "r") as file: additional_projects = json.load(file) - except FileNotFoundError: # noqa + except FileNotFoundError: # noqa _logger.info("No local file for user projects") except Exception as e: _logger.error(e) @@ -91,8 +91,9 @@ def get_additional_user_projects_from_file(username): if additional_projects: if not additional_projects.get(username, False): _logger.info("No projects for user '%s' on local file" % username) - elif not isinstance(additional_projects[username], list) \ - or not isinstance(additional_projects[username][0], str): + elif not isinstance(additional_projects[username], list) or not isinstance( + additional_projects[username][0], str + ): _logger.error("Projects on file are not list of strings") else: user_projects.update(p for p in additional_projects[username]) @@ -107,12 +108,11 @@ def check_user_groups_against_groups(cls, request, group_list): Return True if there is a match, otherwise False. """ - assert request.user is not None, 'request.user is None' - assert request.user.token is not None, 'request.user.token is None' + assert request.user is not None, "request.user is None" + assert request.user.token is not None, "request.user.token is None" user_projects = set( - group.split(':')[-1] - for group in request.user.token.get('group_names', []) + group.split(":")[-1] for group in request.user.token.get("group_names", []) ) user_projects.update(cls.get_additional_user_projects_from_file(request.user.username)) @@ -131,7 +131,7 @@ def check_services_against_allowed_services(cls, request, service_list): Return True if there is a match, otherwise False. """ - assert request.user.username is not None, 'request.user.username is None' + assert request.user.username is not None, "request.user.username is None" authenticated_service = request.user.username diff --git a/src/metax_api/services/callable_service.py b/src/metax_api/services/callable_service.py index ca204a58..eecf5569 100755 --- a/src/metax_api/services/callable_service.py +++ b/src/metax_api/services/callable_service.py @@ -10,7 +10,7 @@ _logger = logging.getLogger(__name__) -class _CallableService(): +class _CallableService: """ Methods to handle adding and executing callable objects, which will be executed @@ -40,13 +40,13 @@ def run_post_request_callables(self): if not self.post_request_callables: return - _logger.debug('Executing %d post_request_callables...' % len(self.post_request_callables)) + _logger.debug("Executing %d post_request_callables..." % len(self.post_request_callables)) for callable_obj in self.post_request_callables: try: callable_obj() except: - _logger.exception('Failed to execute post_request_callables') + _logger.exception("Failed to execute post_request_callables") # failure to execute a callable should fail the entire request self.clear_callables() raise diff --git a/src/metax_api/services/catalog_record_service.py b/src/metax_api/services/catalog_record_service.py index a40aaf33..3f144272 100755 --- a/src/metax_api/services/catalog_record_service.py +++ b/src/metax_api/services/catalog_record_service.py @@ -5,6 +5,7 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT import logging +import re import urllib.parse from collections import defaultdict from os.path import dirname, join @@ -25,6 +26,7 @@ remove_keys_recursively, ) +from .auth_service import AuthService from .common_service import CommonService from .datacite_service import DataciteService from .file_service import FileService @@ -39,8 +41,8 @@ except ImportError as e: _logger.error(e) -class CatalogRecordService(CommonService, ReferenceDataMixin): +class CatalogRecordService(CommonService, ReferenceDataMixin): @classmethod def get_queryset_search_params(cls, request): """ @@ -54,97 +56,105 @@ def get_queryset_search_params(cls, request): queryset_search_params = cls.filter_by_state(request, queryset_search_params) - if request.query_params.get('state', False): - state_vals = request.query_params['state'].split(',') + if request.query_params.get("state", False): + state_vals = request.query_params["state"].split(",") for val in state_vals: try: int(val) except ValueError: - raise Http400({ 'state': ['Value \'%s\' is not an integer' % val] }) - queryset_search_params['preservation_state__in'] = state_vals + raise Http400({"state": ["Value '%s' is not an integer" % val]}) + queryset_search_params["preservation_state__in"] = state_vals - if request.query_params.get('preservation_state', False): - state_vals = request.query_params['preservation_state'].split(',') + if request.query_params.get("preservation_state", False): + state_vals = request.query_params["preservation_state"].split(",") for val in state_vals: try: int(val) except ValueError: - raise Http400({ 'preservation_state': ['Value \'%s\' is not an integer' % val] }) - queryset_search_params['preservation_state__in'] = state_vals - - if CommonService.get_boolean_query_param(request, 'latest'): - queryset_search_params['next_dataset_version_id'] = None - - if request.query_params.get('deprecated', None) is not None: - queryset_search_params['deprecated'] = CommonService.get_boolean_query_param(request, 'deprecated') - - if request.query_params.get('curator', False): - queryset_search_params['research_dataset__contains'] = \ - {'curator': [{ 'identifier': request.query_params['curator']}]} - - if request.query_params.get('owner_id', False): - queryset_search_params['editor__contains'] = { 'owner_id': request.query_params['owner_id'] } - - if request.query_params.get('user_created', False): - queryset_search_params['user_created'] = request.query_params['user_created'] - - if request.query_params.get('editor', False): - queryset_search_params['editor__contains'] = { 'identifier': request.query_params['editor'] } - - if request.query_params.get('metadata_provider_user', False): - queryset_search_params['metadata_provider_user'] = request.query_params['metadata_provider_user'] - - if request.query_params.get('metadata_owner_org', False): - queryset_search_params['metadata_owner_org__in'] = request.query_params['metadata_owner_org'].split(',') - - if request.query_params.get('contract_org_identifier', False): - if request.user.username not in ('metax', 'tpas'): - raise Http403({ 'detail': ['query parameter pas_filter is restricted']}) - queryset_search_params['contract__contract_json__organization__organization_identifier__iregex'] = \ - request.query_params['contract_org_identifier'] - - if request.query_params.get('pas_filter', False): + raise Http400({"preservation_state": ["Value '%s' is not an integer" % val]}) + queryset_search_params["preservation_state__in"] = state_vals + + if CommonService.get_boolean_query_param(request, "latest"): + queryset_search_params["next_dataset_version_id"] = None + + if request.query_params.get("deprecated", None) is not None: + queryset_search_params["deprecated"] = CommonService.get_boolean_query_param( + request, "deprecated" + ) + + if request.query_params.get("curator", False): + queryset_search_params["research_dataset__contains"] = { + "curator": [{"identifier": request.query_params["curator"]}] + } + + if request.query_params.get("user_created", False): + queryset_search_params["user_created"] = request.query_params["user_created"] + + if request.query_params.get("metadata_provider_user", False): + queryset_search_params["metadata_provider_user"] = request.query_params[ + "metadata_provider_user" + ] + + if request.query_params.get("metadata_owner_org", False): + queryset_search_params["metadata_owner_org__in"] = request.query_params[ + "metadata_owner_org" + ].split(",") + + if request.query_params.get("contract_org_identifier", False): + if request.user.username not in ("metax", "tpas"): + raise Http403({"detail": ["query parameter pas_filter is restricted"]}) + queryset_search_params[ + "contract__contract_json__organization__organization_identifier__iregex" + ] = request.query_params["contract_org_identifier"] + + if request.query_params.get("pas_filter", False): cls.set_pas_filter(queryset_search_params, request) + if request.query_params.get("projects", False): + cls.set_projects_filter(queryset_search_params, request) + if CommonService.has_research_agent_query_params(request): cls.set_actor_filters(queryset_search_params, request) - if request.query_params.get('data_catalog', False): - queryset_search_params['data_catalog__catalog_json__identifier__iregex'] = \ - request.query_params['data_catalog'] + if request.query_params.get("data_catalog", False): + queryset_search_params[ + "data_catalog__catalog_json__identifier__iregex" + ] = request.query_params["data_catalog"] - if request.query_params.get('api_version', False): + if request.query_params.get("api_version", False): try: - value = int(request.query_params['api_version']) + value = int(request.query_params["api_version"]) except ValueError: - value = request.query_params['api_version'] - raise Http400({ 'api_version': ['Value \'%s\' is not an integer' % value] }) + value = request.query_params["api_version"] + raise Http400({"api_version": ["Value '%s' is not an integer" % value]}) - queryset_search_params['api_meta__contains'] = { 'version': value } + queryset_search_params["api_meta__contains"] = {"version": value} return queryset_search_params @staticmethod def filter_by_state(request, queryset_search_params): - ''' + """ Helper method to filter returning data by state: unauthenticated users get only published data, and end users get only published & their own drafts - ''' + """ state_filter = None - if request.user.username is None: # unauthenticated user - state_filter = Q(state='published') + if request.user.username == "": # unauthenticated user + state_filter = Q(state="published") elif request.user.is_service: # service account pass - else: # enduser api - state_filter = Q(state='published') | Q(state='draft', metadata_provider_user=request.user.username) + else: # enduser api + state_filter = Q(state="published") | Q( + state="draft", metadata_provider_user=request.user.username + ) if state_filter: - if 'q_filters' in queryset_search_params: - queryset_search_params['q_filters'].append(state_filter) + if "q_filters" in queryset_search_params: + queryset_search_params["q_filters"].append(state_filter) else: - queryset_search_params['q_filters'] = [state_filter] + queryset_search_params["q_filters"] = [state_filter] return queryset_search_params @@ -156,66 +166,113 @@ def set_actor_filters(queryset_search_params, request): Organization filters also search matches from person's "member_of" field. Q-filters from multiple queries are AND'ed together eventually. """ + def _get_person_filter(agent, person): + param = "name" + # check if query parameter is person's ID + if re.search(r"((\d*-\d*)+)", person): + person = "http://orcid.org/" + person + param = "identifier" + name_filter = Q() # only one publisher possible - if agent == 'publisher': - name_filter |= Q(**{ f'research_dataset__{agent}__name__iregex': person }) + if agent == "publisher": + name_filter |= Q(**{f"research_dataset__{agent}__{param}__iregex": person}) else: # having same problem as in set_pas_filter below.. for i in range(3): - name_filter |= Q(**{ f'research_dataset__{agent}__{i}__name__iregex': person }) + name_filter |= Q(**{f"research_dataset__{agent}__{i}__{param}__iregex": person}) - name_filter |= Q(**{ f'research_dataset__{agent}__contains': [{ 'name': person }] }) + name_filter |= Q(**{f"research_dataset__{agent}__contains": [{param: person}]}) # regex will find matches from organization name fields so have to disable it - person_filter = Q(**{ f'research_dataset__{agent}__contains': [{ '@type': "Person" }] }) - name_filter.add(person_filter, 'AND') + person_filter = Q(**{f"research_dataset__{agent}__contains": [{"@type": "Person"}]}) + name_filter.add(person_filter, "AND") return name_filter def _get_org_filter(agent, org): + name = "name" + name_en = "name__en" + name_fi = "name__fi" + # check if query parameter is organizational ID + if re.search(r"\b\d{5}\b", org[:5]): + org = "http://uri.suomi.fi/codelist/fairdata/organization/code/" + org + name = "identifier" + name_en = "identifier" + name_fi = "identifier" + name_filter = Q() # only one publisher possible - if agent == 'publisher': - name_filter |= (Q(**{ f'research_dataset__{agent}__name__en__iregex': org })) - name_filter |= (Q(**{ f'research_dataset__{agent}__name__fi__iregex': org })) - name_filter |= (Q(**{ f'research_dataset__{agent}__member_of__name__en__iregex': org })) - name_filter |= (Q(**{ f'research_dataset__{agent}__member_of__name__fi__iregex': org })) + if agent == "publisher": + name_filter |= Q(**{f"research_dataset__{agent}__{name_en}__iregex": org}) + name_filter |= Q(**{f"research_dataset__{agent}__{name_fi}__iregex": org}) + name_filter |= Q(**{f"research_dataset__{agent}__member_of__{name_en}__iregex": org}) + name_filter |= Q(**{f"research_dataset__{agent}__member_of__{name_fi}__iregex": org}) else: for i in range(3): - name_filter |= (Q(**{ f'research_dataset__{agent}__{i}__name__en__iregex': org })) - name_filter |= (Q(**{ f'research_dataset__{agent}__{i}__name__fi__iregex': org })) - name_filter |= (Q(**{ f'research_dataset__{agent}__{i}__member_of__name__en__iregex': org })) - name_filter |= (Q(**{ f'research_dataset__{agent}__{i}__member_of__name__fi__iregex': org })) - - name_filter |= (Q(**{ f'research_dataset__{agent}__contains': [{ 'name': {'en': org} }] })) - name_filter |= (Q(**{ f'research_dataset__{agent}__contains': [{ 'name': {'fi': org} }] })) - name_filter |= ( - Q(**{ f'research_dataset__{agent}__contains': [{ 'member_of': {'name': {'en': org}} }] }) - ) - name_filter |= ( - Q(**{ f'research_dataset__{agent}__contains': [{ 'member_of': {'name': {'fi': org}} }] }) - ) + name_filter |= Q(**{f"research_dataset__{agent}__{i}__{name_en}__iregex": org}) + name_filter |= Q(**{f"research_dataset__{agent}__{i}__{name_fi}__iregex": org}) + name_filter |= Q( + **{f"research_dataset__{agent}__{i}__member_of__{name_en}__iregex": org} + ) + name_filter |= Q( + **{f"research_dataset__{agent}__{i}__member_of__{name_fi}__iregex": org} + ) + + if name == "name": + name_filter |= Q( + **{f"research_dataset__{agent}__contains": [{name: {"en": org}}]} + ) + name_filter |= Q( + **{f"research_dataset__{agent}__contains": [{name: {"fi": org}}]} + ) + name_filter |= Q( + **{ + f"research_dataset__{agent}__contains": [ + {"member_of": {name: {"en": org}}} + ] + } + ) + name_filter |= Q( + **{ + f"research_dataset__{agent}__contains": [ + {"member_of": {name: {"fi": org}}} + ] + } + ) + else: + name_filter |= Q( + **{f"research_dataset__{agent}__contains": [{name: org}]} + ) + name_filter |= Q( + **{ + f"research_dataset__{agent}__contains": [ + {"member_of": {name: org}} + ] + } + ) return name_filter q_filter = Q() - separator = 'OR' if request.query_params.get('condition_separator', '').upper() == 'OR' else 'AND' + separator = ( + "OR" if request.query_params.get("condition_separator", "").upper() == "OR" else "AND" + ) - for agent in ['creator', 'curator', 'publisher', 'rights_holder']: - if request.query_params.get(f'{agent}_person'): - person = urllib.parse.unquote(request.query_params[f'{agent}_person']) + for agent in ["creator", "curator", "publisher", "rights_holder"]: + if request.query_params.get(f"{agent}_person"): + person = urllib.parse.unquote(request.query_params[f"{agent}_person"]) q_filter.add(_get_person_filter(agent, person), separator) - if request.query_params.get(f'{agent}_organization'): - org = urllib.parse.unquote(request.query_params[f'{agent}_organization']) + if request.query_params.get(f"{agent}_organization"): + org = urllib.parse.unquote(request.query_params[f"{agent}_organization"]) q_filter.add(_get_org_filter(agent, org), separator) - if 'q_filters' in queryset_search_params: # pragma: no cover - queryset_search_params['q_filters'].append(q_filter) + if "q_filters" in queryset_search_params: # pragma: no cover + queryset_search_params["q_filters"].append(q_filter) else: - queryset_search_params['q_filters'] = [q_filter] + queryset_search_params["q_filters"] = [q_filter] @staticmethod def set_pas_filter(queryset_search_params, request): @@ -223,10 +280,10 @@ def set_pas_filter(queryset_search_params, request): A somewhat specific filter for PAS needs... The below OR query is AND'ed with any other possible filters from other query parameters. """ - if request.user.username not in ('metax', 'tpas'): - raise Http403({ 'detail': ['query parameter pas_filter is restricted']}) + if request.user.username not in ("metax", "tpas"): + raise Http403({"detail": ["query parameter pas_filter is restricted"]}) - search_string = urllib.parse.unquote(request.query_params.get('pas_filter', '')) + search_string = urllib.parse.unquote(request.query_params.get("pas_filter", "")) # dataset title, from various languages... q1 = Q(research_dataset__title__en__iregex=search_string) @@ -244,17 +301,45 @@ def set_pas_filter(queryset_search_params, request): q4 = Q(research_dataset__curator__0__name__iregex=search_string) q5 = Q(research_dataset__curator__1__name__iregex=search_string) q6 = Q(research_dataset__curator__2__name__iregex=search_string) - q7 = Q(research_dataset__curator__contains=[{ 'name': search_string }]) + q7 = Q(research_dataset__curator__contains=[{"name": search_string}]) q_filter = q1 | q2 | q3 | q4 | q5 | q6 | q7 - if 'q_filters' in queryset_search_params: # pragma: no cover + if "q_filters" in queryset_search_params: # pragma: no cover # no usecase yet but leaving comment for future reference... if the need arises to # include Q-filters from multiple sources (query params), probably AND them together # by appending to list - queryset_search_params['q_filters'].append(q_filter) + queryset_search_params["q_filters"].append(q_filter) + else: + queryset_search_params["q_filters"] = [q_filter] + + @staticmethod + def set_projects_filter(queryset_search_params, request): + """ + Filter datasets that belong to any project in comma-separated projects list. + + A dataset belongs to the projects of the files it contains. Because this is a many-to-many + relationship that can return duplicate datasets, it's necessary to remove non-distinct values + from query results. + """ + projects = request.query_params.get("projects").split(",") + + # non-service users can only query their own projects + if not request.user.is_service: + user_projects = [] + if request.user.username != "": + user_projects = AuthService.get_user_projects(request) + if not set(projects).issubset(user_projects): + raise Http403({"detail": ["User is not member of project"]}) + + q_filter = Q(files__project_identifier__in=projects) + if "deduplicated_q_filters" in queryset_search_params: + queryset_search_params["deduplicated_q_filters"].append(q_filter) else: - queryset_search_params['q_filters'] = [q_filter] + queryset_search_params["deduplicated_q_filters"] = [q_filter] + + return queryset_search_params + @staticmethod def populate_file_details(cr_json, request): @@ -269,10 +354,13 @@ def populate_file_details(cr_json, request): Note: Some of these results may be very useful to cache, or cache the entire dataset if feasible. """ - from metax_api.api.rest.base.serializers import LightDirectorySerializer, LightFileSerializer + from metax_api.api.rest.base.serializers import ( + LightDirectorySerializer, + LightFileSerializer, + ) - rd = cr_json['research_dataset'] - file_identifiers = [f['identifier'] for f in rd.get('files', [])] + rd = cr_json["research_dataset"] + file_identifiers = [f["identifier"] for f in rd.get("files", [])] # these fields must be retrieved from the db in order to do the mapping, even if they are not # requested when using ?file_fields=... or ?directory_fields=... ditch those fields @@ -283,113 +371,141 @@ def populate_file_details(cr_json, request): directory_fields, file_fields = FileService._get_requested_file_browsing_fields(request) - if 'identifier' not in directory_fields: - directory_fields.append('identifier') + if "identifier" not in directory_fields: + directory_fields.append("identifier") dir_identifier_requested = False - if 'identifier' not in file_fields: - file_fields.append('identifier') + if "identifier" not in file_fields: + file_fields.append("identifier") file_identifier_requested = False for file in File.objects.filter(identifier__in=file_identifiers).values(*file_fields): - for f in rd['files']: - if f['identifier'] == file['identifier']: - f['details'] = LightFileSerializer.serialize(file) + for f in rd["files"]: + if f["identifier"] == file["identifier"]: + f["details"] = LightFileSerializer.serialize(file) continue - dir_identifiers = [dr['identifier'] for dr in rd.get('directories', [])] + dir_identifiers = [dr["identifier"] for dr in rd.get("directories", [])] - for directory in Directory.objects.filter(identifier__in=dir_identifiers).values(*directory_fields): - for dr in rd['directories']: - if dr['identifier'] == directory['identifier']: - dr['details'] = LightDirectorySerializer.serialize(directory) + for directory in Directory.objects.filter(identifier__in=dir_identifiers).values( + *directory_fields + ): + for dr in rd["directories"]: + if dr["identifier"] == directory["identifier"]: + dr["details"] = LightDirectorySerializer.serialize(directory) continue if not dir_identifiers: return - if not directory_fields or ('byte_size' in directory_fields or 'file_count' in directory_fields): + if not directory_fields or ( + "byte_size" in directory_fields or "file_count" in directory_fields + ): # no specific fields requested -> retrieve, # OR byte_size or file_count among requested fields -> retrieve - _directory_data = CatalogRecord.objects.values_list('_directory_data', flat=True) \ - .get(pk=cr_json['id']) + _directory_data = CatalogRecord.objects.values_list("_directory_data", flat=True).get( + pk=cr_json["id"] + ) - for dr in rd['directories']: + for dr in rd["directories"]: - if 'details' not in dr: + if "details" not in dr: # probably the directory did not have its details populated # because the dataset is deprecated and the directory no longer exists continue - FileService.retrieve_directory_byte_sizes_and_file_counts_for_cr(dr['details'], - not_cr_id=None, directory_fields=directory_fields, cr_directory_data=_directory_data) + FileService.retrieve_directory_byte_sizes_and_file_counts_for_cr( + dr["details"], + not_cr_id=None, + directory_fields=directory_fields, + cr_directory_data=_directory_data, + ) # cleanup identifiers, if they were not actually requested if not dir_identifier_requested: - for dr in rd['directories']: - del dr['details']['identifier'] + for dr in rd["directories"]: + del dr["details"]["identifier"] if not file_identifier_requested: - for f in rd['files']: - del f['details']['identifier'] + for f in rd["files"]: + del f["details"]["identifier"] @classmethod - def transform_datasets_to_format(cls, catalog_records_json, target_format, - include_xml_declaration=True, request=None): + def transform_datasets_to_format( + cls, + catalog_records_json, + target_format, + include_xml_declaration=True, + request=None, + ): """ params: catalog_records: a list of catalog record dicts, or a single dict """ - if target_format in ('datacite', 'fairdata_datacite'): + if target_format in ("datacite", "fairdata_datacite"): - is_strict = target_format == 'datacite' + is_strict = target_format == "datacite" dummy_doi = False if request: - dummy_doi = CommonService.get_boolean_query_param(request, 'dummy_doi') + dummy_doi = CommonService.get_boolean_query_param(request, "dummy_doi") return DataciteService().convert_catalog_record_to_datacite_xml( - catalog_records_json, include_xml_declaration, is_strict, dummy_doi=dummy_doi) + catalog_records_json, + include_xml_declaration, + is_strict, + dummy_doi=dummy_doi, + ) def _preprocess_list(key, value): """ Helper function to get right structure for list values. This function is called recursively. """ - if key not in ['item', 'researchdataset'] and isinstance(value, list) and len(value) > 1: - value = {'item': value} + if ( + key not in ["item", "researchdataset"] + and isinstance(value, list) + and len(value) > 1 + ): + value = {"item": value} return key, value if isinstance(catalog_records_json, dict): - content_to_transform = { 'researchdataset': catalog_records_json['research_dataset'] } + content_to_transform = {"researchdataset": catalog_records_json["research_dataset"]} else: - rd_list = { 'researchdataset': (cr['research_dataset'] for cr in catalog_records_json) } - content_to_transform = { 'researchdatasets': rd_list } + rd_list = {"researchdataset": (cr["research_dataset"] for cr in catalog_records_json)} + content_to_transform = {"researchdatasets": rd_list} xml_str = xmltodict.unparse(content_to_transform, preprocessor=_preprocess_list) - xml_str = xml_str.replace('\n', '', 1) + xml_str = xml_str.replace("\n", "", 1) # This is a bit ugly way to put the metax data to the datacite namespace, # which allows us to use the default namespace in xquery files. - xml_str = xml_str.replace('', - '') - if target_format == 'metax': + xml_str = xml_str.replace( + "", + '', + ) + if target_format == "metax": # mostly for debugging purposes, the 'metax xml' can be returned as well return xml_str - target_xslt_file_path = join(dirname(dirname(__file__)), 'api/rest/base/xslt/%s.xslt' % target_format) + target_xslt_file_path = join( + dirname(dirname(__file__)), "api/rest/base/xslt/%s.xslt" % target_format + ) try: with open(target_xslt_file_path) as f: xslt = f.read() except OSError: - raise Http400('Requested format \'%s\' is not available' % target_format) + raise Http400("Requested format '%s' is not available" % target_format) try: transformed_xml = sxq.execute(xslt, xml_str) except: - _logger.exception('Something is wrong with the xslt file at %s:' % target_xslt_file_path) - raise Http503('Requested format \'%s\' is currently unavailable' % target_format) + _logger.exception( + "Something is wrong with the xslt file at %s:" % target_xslt_file_path + ) + raise Http503("Requested format '%s' is currently unavailable" % target_format) if include_xml_declaration: return '%s' % transformed_xml @@ -412,249 +528,414 @@ def validate_reference_data(cls, research_dataset, cache): """ reference_data = cls.get_reference_data(cache) # ic(reference_data) - refdata = reference_data['reference_data'] - orgdata = reference_data['organization_data']['organization'] + refdata = reference_data["reference_data"] + orgdata = reference_data["organization_data"]["organization"] errors = defaultdict(list) - for theme in research_dataset.get('theme', []): - ref_entry = cls.check_ref_data(refdata['keyword'], theme['identifier'], - 'research_dataset.theme.identifier', errors) + for theme in research_dataset.get("theme", []): + ref_entry = cls.check_ref_data( + refdata["keyword"], + theme["identifier"], + "research_dataset.theme.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, theme, label_field='pref_label') - - for fos in research_dataset.get('field_of_science', []): - ref_entry = cls.check_ref_data(refdata['field_of_science'], fos['identifier'], - 'research_dataset.field_of_science.identifier', errors) + cls.populate_from_ref_data(ref_entry, theme, label_field="pref_label") + + for fos in research_dataset.get("field_of_science", []): + ref_entry = cls.check_ref_data( + refdata["field_of_science"], + fos["identifier"], + "research_dataset.field_of_science.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, fos, label_field='pref_label') + cls.populate_from_ref_data(ref_entry, fos, label_field="pref_label") - for remote_resource in research_dataset.get('remote_resources', []): + for remote_resource in research_dataset.get("remote_resources", []): - for license in remote_resource.get('license', []): - license_id = license.get('identifier', False) - license_url = license.get('license', False) + for license in remote_resource.get("license", []): + license_id = license.get("identifier", False) + license_url = license.get("license", False) if license_id: - ref_entry = cls.check_ref_data(refdata['license'], license['identifier'], - 'research_dataset.remote_resources.license.identifier', errors) + ref_entry = cls.check_ref_data( + refdata["license"], + license["identifier"], + "research_dataset.remote_resources.license.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, license, label_field='title', add_in_scheme=False) + cls.populate_from_ref_data( + ref_entry, license, label_field="title", add_in_scheme=False + ) # Populate license field from reference data only if it is empty, i.e. not provided by the user # and when the reference data license has a same_as entry - if not license_url and ref_entry.get('same_as', False): - license_url = ref_entry['same_as'] + if not license_url and ref_entry.get("same_as", False): + license_url = ref_entry["same_as"] if license_url: - license['license'] = license_url - - if remote_resource.get('resource_type', False): - ref_entry = cls.check_ref_data(refdata['resource_type'], remote_resource['resource_type']['identifier'], - 'research_dataset.remote_resources.resource_type.identifier', errors) + license["license"] = license_url + + if remote_resource.get("resource_type", False): + ref_entry = cls.check_ref_data( + refdata["resource_type"], + remote_resource["resource_type"]["identifier"], + "research_dataset.remote_resources.resource_type.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, remote_resource['resource_type'], label_field='pref_label') - - if remote_resource.get('file_type', False): - ref_entry = cls.check_ref_data(refdata['file_type'], remote_resource['file_type']['identifier'], - 'research_dataset.remote_resources.file_type.identifier', errors) + cls.populate_from_ref_data( + ref_entry, + remote_resource["resource_type"], + label_field="pref_label", + ) + + if remote_resource.get("file_type", False): + ref_entry = cls.check_ref_data( + refdata["file_type"], + remote_resource["file_type"]["identifier"], + "research_dataset.remote_resources.file_type.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, remote_resource['file_type'], label_field='pref_label') - - if remote_resource.get('use_category', False): - ref_entry = cls.check_ref_data(refdata['use_category'], remote_resource['use_category']['identifier'], - 'research_dataset.remote_resources.use_category.identifier', errors) + cls.populate_from_ref_data( + ref_entry, + remote_resource["file_type"], + label_field="pref_label", + ) + + if remote_resource.get("use_category", False): + ref_entry = cls.check_ref_data( + refdata["use_category"], + remote_resource["use_category"]["identifier"], + "research_dataset.remote_resources.use_category.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, remote_resource['use_category'], label_field='pref_label') - - for language in research_dataset.get('language', []): - ref_entry = cls.check_ref_data(refdata['language'], language['identifier'], - 'research_dataset.language.identifier', errors) + cls.populate_from_ref_data( + ref_entry, + remote_resource["use_category"], + label_field="pref_label", + ) + + for language in research_dataset.get("language", []): + ref_entry = cls.check_ref_data( + refdata["language"], + language["identifier"], + "research_dataset.language.identifier", + errors, + ) if ref_entry: - label_field = 'title' - cls.populate_from_ref_data(ref_entry, language, label_field=label_field, add_in_scheme=False) + label_field = "title" + cls.populate_from_ref_data( + ref_entry, language, label_field=label_field, add_in_scheme=False + ) cls.remove_language_obj_irrelevant_titles(language, label_field) - access_rights = research_dataset.get('access_rights', None) + access_rights = research_dataset.get("access_rights", None) if access_rights: - if 'access_type' in access_rights: - ref_entry = cls.check_ref_data(refdata['access_type'], access_rights['access_type']['identifier'], - 'research_dataset.access_rights.access_type.identifier', errors) + if "access_type" in access_rights: + ref_entry = cls.check_ref_data( + refdata["access_type"], + access_rights["access_type"]["identifier"], + "research_dataset.access_rights.access_type.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, access_rights['access_type'], label_field='pref_label') - - for rg in access_rights.get('restriction_grounds', []): - ref_entry = cls.check_ref_data(refdata['restriction_grounds'], rg['identifier'], - 'research_dataset.access_rights.restriction_grounds.identifier', errors) + cls.populate_from_ref_data( + ref_entry, + access_rights["access_type"], + label_field="pref_label", + ) + + for rg in access_rights.get("restriction_grounds", []): + ref_entry = cls.check_ref_data( + refdata["restriction_grounds"], + rg["identifier"], + "research_dataset.access_rights.restriction_grounds.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, rg, label_field='pref_label') + cls.populate_from_ref_data(ref_entry, rg, label_field="pref_label") - for license in access_rights.get('license', []): - license_id = license.get('identifier', False) - license_url = license.get('license', False) + for license in access_rights.get("license", []): + license_id = license.get("identifier", False) + license_url = license.get("license", False) if license_id: - ref_entry = cls.check_ref_data(refdata['license'], license_id, - 'research_dataset.access_rights.license.identifier', errors) + ref_entry = cls.check_ref_data( + refdata["license"], + license_id, + "research_dataset.access_rights.license.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, license, label_field='title', add_in_scheme=False) + cls.populate_from_ref_data( + ref_entry, license, label_field="title", add_in_scheme=False + ) # Populate license field from reference data only if it is empty, i.e. not provided by the user # and when the reference data license has a same_as entry - if not license_url and ref_entry.get('same_as', False): - license_url = ref_entry['same_as'] + if not license_url and ref_entry.get("same_as", False): + license_url = ref_entry["same_as"] if license_url: - license['license'] = license_url - - for project in research_dataset.get('is_output_of', []): - for org_obj in project.get('source_organization', []): - cls.process_org_obj_against_ref_data(orgdata, org_obj, - 'research_dataset.is_output_of.source_organization', - refdata=refdata, errors=errors) - - for org_obj in project.get('has_funding_agency', []): - cls.process_org_obj_against_ref_data(orgdata, org_obj, - 'research_dataset.is_output_of.has_funding_agency', - refdata=refdata, errors=errors) - - if project.get('funder_type', False): - ref_entry = cls.check_ref_data(refdata['funder_type'], project['funder_type']['identifier'], - 'research_dataset.is_output_of.funder_type.identifier', errors) - if ref_entry: - cls.populate_from_ref_data(ref_entry, project['funder_type'], label_field='pref_label') + license["license"] = license_url + + for project in research_dataset.get("is_output_of", []): + for org_obj in project.get("source_organization", []): + cls.process_org_obj_against_ref_data( + orgdata, + org_obj, + "research_dataset.is_output_of.source_organization", + refdata=refdata, + errors=errors, + ) - for other_identifier in research_dataset.get('other_identifier', []): - if other_identifier.get('type', False): - ref_entry = cls.check_ref_data(refdata['identifier_type'], other_identifier['type']['identifier'], - 'research_dataset.other_identifier.type.identifier', errors) - if ref_entry: - cls.populate_from_ref_data(ref_entry, other_identifier['type'], label_field='pref_label') + for org_obj in project.get("has_funding_agency", []): + cls.process_org_obj_against_ref_data( + orgdata, + org_obj, + "research_dataset.is_output_of.has_funding_agency", + refdata=refdata, + errors=errors, + ) - if other_identifier.get('provider', False): - cls.process_org_obj_against_ref_data(orgdata, other_identifier['provider'], - 'research_dataset.other_identifier.provider', refdata=refdata, - errors=errors) + if project.get("funder_type", False): + ref_entry = cls.check_ref_data( + refdata["funder_type"], + project["funder_type"]["identifier"], + "research_dataset.is_output_of.funder_type.identifier", + errors, + ) + if ref_entry: + cls.populate_from_ref_data( + ref_entry, project["funder_type"], label_field="pref_label" + ) + + for other_identifier in research_dataset.get("other_identifier", []): + if other_identifier.get("type", False): + ref_entry = cls.check_ref_data( + refdata["identifier_type"], + other_identifier["type"]["identifier"], + "research_dataset.other_identifier.type.identifier", + errors, + ) + if ref_entry: + cls.populate_from_ref_data( + ref_entry, other_identifier["type"], label_field="pref_label" + ) + + if other_identifier.get("provider", False): + cls.process_org_obj_against_ref_data( + orgdata, + other_identifier["provider"], + "research_dataset.other_identifier.provider", + refdata=refdata, + errors=errors, + ) - for spatial in research_dataset.get('spatial', []): - as_wkt = spatial.get('as_wkt', []) + for spatial in research_dataset.get("spatial", []): + as_wkt = spatial.get("as_wkt", []) - if spatial.get('place_uri', False): - place_uri = spatial.get('place_uri') - ref_entry = cls.check_ref_data(refdata['location'], place_uri['identifier'], - 'research_dataset.spatial.place_uri.identifier', errors) + if spatial.get("place_uri", False): + place_uri = spatial.get("place_uri") + ref_entry = cls.check_ref_data( + refdata["location"], + place_uri["identifier"], + "research_dataset.spatial.place_uri.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, place_uri, label_field='pref_label') + cls.populate_from_ref_data(ref_entry, place_uri, label_field="pref_label") # Populate as_wkt field from reference data only if it is empty, i.e. not provided by the user # and when the coordinates are available in the reference data - if not as_wkt and ref_entry.get('wkt', False): - as_wkt.append(ref_entry.get('wkt')) + if not as_wkt and ref_entry.get("wkt", False): + as_wkt.append(ref_entry.get("wkt")) if as_wkt: - spatial['as_wkt'] = as_wkt - - for file in research_dataset.get('files', []): - if file.get('file_type', False): - ref_entry = cls.check_ref_data(refdata['file_type'], file['file_type']['identifier'], - 'research_dataset.files.file_type.identifier', errors) + spatial["as_wkt"] = as_wkt + + for file in research_dataset.get("files", []): + if file.get("file_type", False): + ref_entry = cls.check_ref_data( + refdata["file_type"], + file["file_type"]["identifier"], + "research_dataset.files.file_type.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, file['file_type'], label_field='pref_label') - - if file.get('use_category', False): - ref_entry = cls.check_ref_data(refdata['use_category'], file['use_category']['identifier'], - 'research_dataset.files.use_category.identifier', errors) + cls.populate_from_ref_data( + ref_entry, file["file_type"], label_field="pref_label" + ) + + if file.get("use_category", False): + ref_entry = cls.check_ref_data( + refdata["use_category"], + file["use_category"]["identifier"], + "research_dataset.files.use_category.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, file['use_category'], label_field='pref_label') - - for directory in research_dataset.get('directories', []): - if directory.get('use_category', False): - ref_entry = cls.check_ref_data(refdata['use_category'], directory['use_category']['identifier'], - 'research_dataset.directories.use_category.identifier', errors) + cls.populate_from_ref_data( + ref_entry, file["use_category"], label_field="pref_label" + ) + + for directory in research_dataset.get("directories", []): + if directory.get("use_category", False): + ref_entry = cls.check_ref_data( + refdata["use_category"], + directory["use_category"]["identifier"], + "research_dataset.directories.use_category.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, directory['use_category'], label_field='pref_label') - - for contributor in research_dataset.get('contributor', []): - cls.process_research_agent_obj_with_type(orgdata, refdata, errors, contributor, - 'research_dataset.contributor') - - if research_dataset.get('publisher', False): - cls.process_research_agent_obj_with_type(orgdata, refdata, errors, research_dataset['publisher'], - 'research_dataset.publisher') - - for curator in research_dataset.get('curator', []): - cls.process_research_agent_obj_with_type(orgdata, refdata, errors, curator, 'research_dataset.curator') - - for creator in research_dataset.get('creator', []): - cls.process_research_agent_obj_with_type(orgdata, refdata, errors, creator, 'research_dataset.creator') - - for rights_holder in research_dataset.get('rights_holder', []): - cls.process_research_agent_obj_with_type(orgdata, refdata, errors, rights_holder, - 'research_dataset.rights_holder') - - for activity in research_dataset.get('provenance', []): - for was_associated_with in activity.get('was_associated_with', []): - cls.process_research_agent_obj_with_type(orgdata, refdata, errors, was_associated_with, - 'research_dataset.provenance.was_associated_with') - - if activity.get('spatial', False): - spatial = activity['spatial'] - as_wkt = spatial.get('as_wkt', []) + cls.populate_from_ref_data( + ref_entry, directory["use_category"], label_field="pref_label" + ) + + for contributor in research_dataset.get("contributor", []): + cls.process_research_agent_obj_with_type( + orgdata, refdata, errors, contributor, "research_dataset.contributor" + ) + + if research_dataset.get("publisher", False): + cls.process_research_agent_obj_with_type( + orgdata, + refdata, + errors, + research_dataset["publisher"], + "research_dataset.publisher", + ) + + for curator in research_dataset.get("curator", []): + cls.process_research_agent_obj_with_type( + orgdata, refdata, errors, curator, "research_dataset.curator" + ) + + for creator in research_dataset.get("creator", []): + cls.process_research_agent_obj_with_type( + orgdata, refdata, errors, creator, "research_dataset.creator" + ) + + for rights_holder in research_dataset.get("rights_holder", []): + cls.process_research_agent_obj_with_type( + orgdata, + refdata, + errors, + rights_holder, + "research_dataset.rights_holder", + ) + + for activity in research_dataset.get("provenance", []): + for was_associated_with in activity.get("was_associated_with", []): + cls.process_research_agent_obj_with_type( + orgdata, + refdata, + errors, + was_associated_with, + "research_dataset.provenance.was_associated_with", + ) - if spatial.get('place_uri', False): - place_uri = spatial.get('place_uri') - ref_entry = cls.check_ref_data(refdata['location'], place_uri['identifier'], - 'research_dataset.provenance.spatial.place_uri.identifier', errors) + if activity.get("spatial", False): + spatial = activity["spatial"] + as_wkt = spatial.get("as_wkt", []) + + if spatial.get("place_uri", False): + place_uri = spatial.get("place_uri") + ref_entry = cls.check_ref_data( + refdata["location"], + place_uri["identifier"], + "research_dataset.provenance.spatial.place_uri.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, place_uri, label_field='pref_label') + cls.populate_from_ref_data(ref_entry, place_uri, label_field="pref_label") # Populate as_wkt field from reference data only if it is empty, i.e. not provided by the user # and when the coordinates are available in the reference data - if not as_wkt and ref_entry.get('wkt', False): - as_wkt.append(ref_entry.get('wkt')) + if not as_wkt and ref_entry.get("wkt", False): + as_wkt.append(ref_entry.get("wkt")) if as_wkt: - spatial['as_wkt'] = as_wkt - - if activity.get('lifecycle_event', False): - ref_entry = cls.check_ref_data(refdata['lifecycle_event'], - activity['lifecycle_event']['identifier'], - 'research_dataset.provenance.lifecycle_event.identifier', errors) + spatial["as_wkt"] = as_wkt + + if activity.get("lifecycle_event", False): + ref_entry = cls.check_ref_data( + refdata["lifecycle_event"], + activity["lifecycle_event"]["identifier"], + "research_dataset.provenance.lifecycle_event.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, activity['lifecycle_event'], label_field='pref_label') - - if activity.get('preservation_event', False): - ref_entry = cls.check_ref_data(refdata['preservation_event'], - activity['preservation_event']['identifier'], - 'research_dataset.provenance.preservation_event.identifier', errors) + cls.populate_from_ref_data( + ref_entry, activity["lifecycle_event"], label_field="pref_label" + ) + + if activity.get("preservation_event", False): + ref_entry = cls.check_ref_data( + refdata["preservation_event"], + activity["preservation_event"]["identifier"], + "research_dataset.provenance.preservation_event.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, activity['preservation_event'], label_field='pref_label') - - if activity.get('event_outcome', False): - ref_entry = cls.check_ref_data(refdata['event_outcome'], - activity['event_outcome']['identifier'], - 'research_dataset.provenance.event_outcome.identifier', errors) + cls.populate_from_ref_data( + ref_entry, + activity["preservation_event"], + label_field="pref_label", + ) + + if activity.get("event_outcome", False): + ref_entry = cls.check_ref_data( + refdata["event_outcome"], + activity["event_outcome"]["identifier"], + "research_dataset.provenance.event_outcome.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, activity['event_outcome'], label_field='pref_label') - for infra in research_dataset.get('infrastructure', []): - ref_entry = cls.check_ref_data(refdata['research_infra'], infra['identifier'], - 'research_dataset.infrastructure.identifier', errors) + cls.populate_from_ref_data( + ref_entry, activity["event_outcome"], label_field="pref_label" + ) + for infra in research_dataset.get("infrastructure", []): + ref_entry = cls.check_ref_data( + refdata["research_infra"], + infra["identifier"], + "research_dataset.infrastructure.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, infra, label_field='pref_label') - - for relation in research_dataset.get('relation', []): - if relation.get('relation_type', False): - ref_entry = cls.check_ref_data(refdata['relation_type'], relation['relation_type']['identifier'], - 'research_dataset.relation.relation_type.identifier', errors) + cls.populate_from_ref_data(ref_entry, infra, label_field="pref_label") + + for relation in research_dataset.get("relation", []): + if relation.get("relation_type", False): + ref_entry = cls.check_ref_data( + refdata["relation_type"], + relation["relation_type"]["identifier"], + "research_dataset.relation.relation_type.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, relation['relation_type'], label_field='pref_label') + cls.populate_from_ref_data( + ref_entry, relation["relation_type"], label_field="pref_label" + ) - if relation.get('entity', False) and relation.get('entity').get('type', False): + if relation.get("entity", False) and relation.get("entity").get("type", False): - ref_entry = cls.check_ref_data(refdata['resource_type'], relation['entity']['type']['identifier'], - 'research_dataset.relation.entity.type.identifier', errors) + ref_entry = cls.check_ref_data( + refdata["resource_type"], + relation["entity"]["type"]["identifier"], + "research_dataset.relation.entity.type.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, relation['entity']['type'], label_field='pref_label') + cls.populate_from_ref_data( + ref_entry, relation["entity"]["type"], label_field="pref_label" + ) if errors: raise ValidationError(errors) @@ -668,7 +949,7 @@ def remove_contact_info_metadata(cls, rd): :param rd: :return: research dataset with removed contact information """ - return remove_keys_recursively(rd, ['email', 'telephone', 'phone']) + return remove_keys_recursively(rd, ["email", "telephone", "phone"]) @classmethod def check_and_remove_metadata_based_on_access_type(cls, rd): @@ -681,21 +962,25 @@ def check_and_remove_metadata_based_on_access_type(cls, rd): """ access_type_id = cls.get_research_dataset_access_type(rd) - if access_type_id == ACCESS_TYPES['open']: + if access_type_id == ACCESS_TYPES["open"]: pass - elif access_type_id == ACCESS_TYPES['login']: + elif access_type_id == ACCESS_TYPES["login"]: pass - elif access_type_id == ACCESS_TYPES['permit']: + elif access_type_id == ACCESS_TYPES["permit"]: # TODO: # If user does not have rems permission for the catalog record, strip it: # cls._strip_file_and_directory_metadata(rd) # strip always for now. Remove this part when rems checking is implemented cls._strip_file_and_directory_metadata(rd) - elif access_type_id == ACCESS_TYPES['embargo']: + elif access_type_id == ACCESS_TYPES["embargo"]: try: - embargo_time_passed = get_tz_aware_now_without_micros() >= \ - parse_timestamp_string_to_tz_aware_datetime(cls.get_research_dataset_embargo_available(rd)) + embargo_time_passed = ( + get_tz_aware_now_without_micros() + >= parse_timestamp_string_to_tz_aware_datetime( + cls.get_research_dataset_embargo_available(rd) + ) + ) except Exception as e: _logger.error(e) embargo_time_passed = False @@ -723,13 +1008,13 @@ def _strip_file_metadata(cls, rd): :param rd: """ - file_keys_to_leave = set(['title', 'use_category', 'file_type', 'details']) - details_keys_to_leave = set(['byte_size']) + file_keys_to_leave = set(["title", "use_category", "file_type", "details"]) + details_keys_to_leave = set(["byte_size"]) - for file in rd.get('files', []): + for file in rd.get("files", []): leave_keys_in_dict(file, file_keys_to_leave) - if 'details' in file: - leave_keys_in_dict(file['details'], details_keys_to_leave) + if "details" in file: + leave_keys_in_dict(file["details"], details_keys_to_leave) @classmethod def _strip_directory_metadata(cls, rd): @@ -741,33 +1026,33 @@ def _strip_directory_metadata(cls, rd): :param rd: """ - dir_keys_to_leave = set(['title', 'use_category', 'details']) - details_keys_to_leave = set(['byte_size']) + dir_keys_to_leave = set(["title", "use_category", "details"]) + details_keys_to_leave = set(["byte_size"]) - for dir in rd.get('directories', []): + for dir in rd.get("directories", []): leave_keys_in_dict(dir, dir_keys_to_leave) - if 'details' in dir: - leave_keys_in_dict(dir['details'], details_keys_to_leave) + if "details" in dir: + leave_keys_in_dict(dir["details"], details_keys_to_leave) @staticmethod def get_research_dataset_access_type(rd): - return rd.get('access_rights', {}).get('access_type', {}).get('identifier', '') + return rd.get("access_rights", {}).get("access_type", {}).get("identifier", "") @staticmethod def get_research_dataset_embargo_available(rd): - return rd.get('access_rights', {}).get('available', '') + return rd.get("access_rights", {}).get("available", "") @staticmethod def get_research_dataset_license_url(rd): """ Return identifier of the first license if there is a license at all """ - if not rd.get('access_rights', {}).get('license'): + if not rd.get("access_rights", {}).get("license"): return {} - license = rd['access_rights']['license'][0] + license = rd["access_rights"]["license"][0] - return license.get('identifier') or license.get('license') + return license.get("identifier") or license.get("license") @classmethod def destroy_bulk(cls, request): @@ -775,7 +1060,7 @@ def destroy_bulk(cls, request): Mark datasets as deleted en masse. Parameter cr_identifiers can be a list of pk's (integers), or file identifiers (strings). """ - _logger.info('Begin bulk delete datasets') + _logger.info("Begin bulk delete datasets") cr_ids = cls.identifiers_to_ids(request.data) cr_deleted = [] @@ -791,10 +1076,10 @@ def destroy_bulk(cls, request): pass if sorted(no_access) == sorted(cr_ids): - raise Http403({ 'detail': ['None of datasets exists or are permitted for users']}) + raise Http403({"detail": ["None of datasets exists or are permitted for users"]}) if not cr_deleted: return Response(cr_deleted, status=status.HTTP_404_NOT_FOUND) - _logger.info(f'Marked datasets {cr_deleted} as deleted') + _logger.info(f"Marked datasets {cr_deleted} as deleted") return Response(cr_deleted, status=status.HTTP_200_OK) diff --git a/src/metax_api/services/catalog_record_service_v2.py b/src/metax_api/services/catalog_record_service_v2.py index 92ca473f..a51c6b75 100755 --- a/src/metax_api/services/catalog_record_service_v2.py +++ b/src/metax_api/services/catalog_record_service_v2.py @@ -12,25 +12,24 @@ class CatalogRecordServiceV2(CatalogRecordService): - @classmethod def get_queryset_search_params(cls, request): """ v1 API has query params state and preservation_state doing the same thing. v2 API will properly use state to filter by field state. """ - state = request.query_params.get('state', None) + state = request.query_params.get("state", None) if state: # note: request.query_params is a @property of the request object. can not be directly edited. # see rest_framework/request.py request._request.GET._mutable = True - request.query_params.pop('state', None) + request.query_params.pop("state", None) request._request.GET._mutable = False queryset_search_params = super().get_queryset_search_params(request) if state: - queryset_search_params['state'] = state + queryset_search_params["state"] = state return queryset_search_params diff --git a/src/metax_api/services/common_service.py b/src/metax_api/services/common_service.py index 44e01d3b..9be5abdd 100755 --- a/src/metax_api/services/common_service.py +++ b/src/metax_api/services/common_service.py @@ -17,13 +17,15 @@ from metax_api.exceptions import Http400, Http412 from metax_api.models import CatalogRecord as cr, File -from metax_api.utils import get_tz_aware_now_without_micros, parse_timestamp_string_to_tz_aware_datetime +from metax_api.utils import ( + get_tz_aware_now_without_micros, + parse_timestamp_string_to_tz_aware_datetime, +) _logger = logging.getLogger(__name__) -class CommonService(): - +class CommonService: @staticmethod def is_primary_key(received_lookup_value): if not received_lookup_value: @@ -47,14 +49,14 @@ def get_boolean_query_param(request, param_name): else: # if method is called before a view's dispatch() method, the only request available # is a low level WSGIRequest. - for query_param in (val for val in request.environ['QUERY_STRING'].split('&')): + for query_param in (val for val in request.environ["QUERY_STRING"].split("&")): try: - param, val = query_param.split('=') + param, val = query_param.split("=") except ValueError: # probably error was 'cant unpack tuple, not enough values' -> was a # param without value specified, such as ?recursive, instead of ?recursive=true. # if flag is specified without value, default value is to be considered True. - param, val = query_param, 'true' + param, val = query_param, "true" if param == param_name: value = val @@ -62,16 +64,16 @@ def get_boolean_query_param(request, param_name): else: return False - if value in ('', 'true'): + if value in ("", "true"): # flag was specified without value (?recursive), or with value (?recursive=true) return True - elif value in (None, 'false'): + elif value in (None, "false"): # flag was not present, or its value was ?recursive=false return False else: - raise ValidationError({ param_name: [ - 'boolean value must be true or false. received value was %s' % value - ]}) + raise ValidationError( + {param_name: ["boolean value must be true or false. received value was %s" % value]} + ) @staticmethod def get_list_query_param(request, param_name): @@ -87,10 +89,10 @@ def get_list_query_param(request, param_name): if value is None: return None - elif value in ('', ','): + elif value in ("", ","): return set() - values_set = set( v.strip() for v in value.split(',') ) + values_set = set(v.strip() for v in value.split(",")) if values_set: return values_set @@ -104,10 +106,10 @@ def has_research_agent_query_params(request): Queries are for example 'creator_person' or 'publisher_organization' Returns boolean """ - fields = ['creator', 'curator', 'publisher', 'rights_holder'] - types = ['organization', 'person'] + fields = ["creator", "curator", "publisher", "rights_holder"] + types = ["organization", "person"] for field in fields: - if any(request.query_params.get(f'{field}_{type}') for type in types): + if any(request.query_params.get(f"{field}_{type}") for type in types): return True return False @@ -125,26 +127,26 @@ def create_bulk(cls, request, serializer_class, **kwargs): serializer_class: does the actual saving, knows what kind of object is in question """ common_info = cls.update_common_info(request, return_only=True) - kwargs['context']['request'] = request + kwargs["context"]["request"] = request results = None if not request.data: - raise Http400('Request body is required') + raise Http400("Request body is required") if isinstance(request.data, list): if len(request.data) == 0: - raise ValidationError(['the received object list is empty']) + raise ValidationError(["the received object list is empty"]) # dont fail the entire request if only some inserts fail. # successfully created rows are added to 'successful', and # failed inserts are added to 'failed', with a related error message. - results = { 'success': [], 'failed': []} + results = {"success": [], "failed": []} cls._create_bulk(common_info, request.data, results, serializer_class, **kwargs) - if results['success']: + if results["success"]: # if even one insert was successful, general status of the request is success http_status = status.HTTP_201_CREATED else: @@ -152,9 +154,11 @@ def create_bulk(cls, request, serializer_class, **kwargs): http_status = status.HTTP_400_BAD_REQUEST else: - results, http_status = cls._create_single(common_info, request.data, serializer_class, **kwargs) + results, http_status = cls._create_single( + common_info, request.data, serializer_class, **kwargs + ) - if 'failed' in results: + if "failed" in results: cls._check_and_raise_atomic_error(request, results) return results, http_status @@ -184,7 +188,7 @@ def _create_bulk(cls, common_info, initial_data_list, results, serializer_class, cls._append_error(results, serializer, e) else: serializer.save(**common_info) - results['success'].append({ 'object': serializer.data }) + results["success"].append({"object": serializer.data}) @staticmethod def get_json_schema(schema_folder_path, model_name, data_catalog_prefix=False): @@ -195,27 +199,27 @@ def get_json_schema(schema_folder_path, model_name, data_catalog_prefix=False): For datasets, a data catalog prefix can be given, in which case it will be the prefix for the schema file name. """ - schema_name = '' + schema_name = "" - if model_name == 'dataset': + if model_name == "dataset": if data_catalog_prefix: schema_name = data_catalog_prefix else: - schema_name = 'ida' + schema_name = "ida" - schema_name += '_' + schema_name += "_" - schema_name += '%s_schema.json' % model_name + schema_name += "%s_schema.json" % model_name try: - with open('%s/%s' % (schema_folder_path, schema_name), encoding='utf-8') as f: + with open("%s/%s" % (schema_folder_path, schema_name), encoding="utf-8") as f: return json_load(f) except IOError as e: - if model_name != 'dataset': + if model_name != "dataset": # only datasets have a default schema raise _logger.warning(e) - with open('%s/ida_dataset_schema.json' % schema_folder_path, encoding='utf-8') as f: + with open("%s/ida_dataset_schema.json" % schema_folder_path, encoding="utf-8") as f: return json_load(f) @classmethod @@ -242,15 +246,20 @@ def update_bulk(cls, request, model_obj, serializer_class, **kwargs): """ if not isinstance(request.data, list): - raise ValidationError({ 'detail': ['request.data is not a list'] }) + raise ValidationError({"detail": ["request.data is not a list"]}) common_info = cls.update_common_info(request, return_only=True) - results = { 'success': [], 'failed': []} + results = {"success": [], "failed": []} for row in request.data: - instance = cls._get_object_for_update(request, model_obj, row, results, - cls._request_has_header(request, 'HTTP_IF_UNMODIFIED_SINCE')) + instance = cls._get_object_for_update( + request, + model_obj, + row, + results, + cls._request_has_header(request, "HTTP_IF_UNMODIFIED_SINCE"), + ) if not instance: continue @@ -264,15 +273,15 @@ def update_bulk(cls, request, model_obj, serializer_class, **kwargs): cls._append_error(results, serializer, e) else: serializer.save(**common_info) - results['success'].append({ 'object': serializer.data }) + results["success"].append({"object": serializer.data}) # if even one operation was successful, general status of the request is success - if len(results.get('success', [])) > 0: + if len(results.get("success", [])) > 0: http_status = status.HTTP_200_OK else: http_status = status.HTTP_400_BAD_REQUEST - if 'failed' in results: + if "failed" in results: cls._check_and_raise_atomic_error(request, results) return results, http_status @@ -288,31 +297,33 @@ def update_common_info(request, return_only=False): return the common info, so that its info can be used manually, instead of updating request.data here automatically. For that purpose, use the return_only flag. """ - if not request.user.username: # pragma: no cover + if not request.user.username: # pragma: no cover # should never happen: update_common_info is executed only on update operations, # which requires authorization, which should put the username into the request obj. - ValidationError({ - 'detail': 'request.user.username not set; unknown service or user. ' - 'how did you get here without passing authorization...?' - }) + ValidationError( + { + "detail": "request.user.username not set; unknown service or user. " + "how did you get here without passing authorization...?" + } + ) method = request.stream and request.stream.method or False current_time = get_tz_aware_now_without_micros() common_info = {} - if method in ('PUT', 'PATCH', 'DELETE'): - common_info['date_modified'] = current_time + if method in ("PUT", "PATCH", "DELETE"): + common_info["date_modified"] = current_time if request.user.is_service: - common_info['service_modified'] = request.user.username + common_info["service_modified"] = request.user.username else: - common_info['user_modified'] = request.user.username - common_info['service_modified'] = None - elif method == 'POST': - common_info['date_created'] = current_time + common_info["user_modified"] = request.user.username + common_info["service_modified"] = None + elif method == "POST": + common_info["date_created"] = current_time if request.user.is_service: - common_info['service_created'] = request.user.username + common_info["service_created"] = request.user.username else: - common_info['user_created'] = request.user.username + common_info["user_created"] = request.user.username else: pass @@ -323,18 +334,23 @@ def update_common_info(request, return_only=False): @staticmethod def _check_and_raise_atomic_error(request, results): - if 'success' in results and not len(results['success']): + if "success" in results and not len(results["success"]): # everything failed anyway, so return normal route even if atomic was used return - if len(results.get('failed', [])) > 0 and request.query_params.get('atomic', None) in ('', 'true'): - raise ValidationError({ - 'success': [], - 'failed': results['failed'], - 'detail': [ - 'request was failed due to parameter atomic=true. all changes were rolled back. ' - 'actual failed rows are listed in the field \"failed\".' - ] - }) + if len(results.get("failed", [])) > 0 and request.query_params.get("atomic", None) in ( + "", + "true", + ): + raise ValidationError( + { + "success": [], + "failed": results["failed"], + "detail": [ + "request was failed due to parameter atomic=true. all changes were rolled back. " + 'actual failed rows are listed in the field "failed".' + ], + } + ) @staticmethod def _append_error(results, serializer, error): @@ -346,16 +362,18 @@ def _append_error(results, serializer, error): is still a crash, and should be fixed. """ try: - results['failed'].append({ 'object': serializer.initial_data, 'errors': serializer.errors }) + results["failed"].append( + {"object": serializer.initial_data, "errors": serializer.errors} + ) except AssertionError: _logger.exception( - 'Looks like serializer.is_valid() tripped - could not access serializer.errors. ' - 'Returning str(e) instead. THIS SHOULD BE FIXED. YES, IM TALKING TO YOU' + "Looks like serializer.is_valid() tripped - could not access serializer.errors. " + "Returning str(e) instead. THIS SHOULD BE FIXED. YES, IM TALKING TO YOU" ) # note that all cases where this happens should be fixed - this is a programming error. # str(e) might show dicts or lists as strings, which would look silly to receiving # humans - results['failed'].append({ 'object': serializer.initial_data, 'errors': str(error) }) + results["failed"].append({"object": serializer.initial_data, "errors": str(error)}) @staticmethod def _get_object_for_update(request, model_obj, row, results, check_unmodified_since): @@ -374,39 +392,48 @@ def _get_object_for_update(request, model_obj, row, results, check_unmodified_si try: instance = model_obj.objects.get(using_dict=row) except model_obj.DoesNotExist: - results['failed'].append({ 'object': row, 'errors': { 'detail': ['object not found'] }}) + results["failed"].append({"object": row, "errors": {"detail": ["object not found"]}}) except ValidationError as e: - results['failed'].append({ 'object': row, 'errors': { 'detail': e.detail } }) + results["failed"].append({"object": row, "errors": {"detail": e.detail}}) if instance and not instance.user_has_access(request): # dont reveal anything from the actual instance ret = {} - if 'id' in row: - ret['id'] = row['id'] - if 'identifier' in row: - ret['identifier'] = row['identifier'] + if "id" in row: + ret["id"] = row["id"] + if "identifier" in row: + ret["identifier"] = row["identifier"] - results['failed'].append({ - 'object': ret, - 'errors': { - 'detail': ['You are not permitted to access this resource.'] + results["failed"].append( + { + "object": ret, + "errors": {"detail": ["You are not permitted to access this resource."]}, } - }) + ) instance = None if instance and check_unmodified_since: - if 'date_modified' not in row: - results['failed'].append({ - 'object': row, - 'errors': { - 'detail': ['Field date_modified is required when the header If-Unmodified-Since is set'] + if "date_modified" not in row: + results["failed"].append( + { + "object": row, + "errors": { + "detail": [ + "Field date_modified is required when the header If-Unmodified-Since is set" + ] + }, + } + ) + elif instance.modified_since(row["date_modified"]): + results["failed"].append( + { + "object": row, + "errors": {"detail": ["Resource has been modified"]}, } - }) - elif instance.modified_since(row['date_modified']): - results['failed'].append({ 'object': row, 'errors': { 'detail': ['Resource has been modified'] } }) + ) else: # good case - all is good pass @@ -416,22 +443,22 @@ def _get_object_for_update(request, model_obj, row, results, check_unmodified_si @classmethod def _validate_and_get_if_unmodified_since_header_as_tz_aware_datetime(cls, request): try: - return cls._validate_http_date_header(request, 'HTTP_IF_UNMODIFIED_SINCE') + return cls._validate_http_date_header(request, "HTTP_IF_UNMODIFIED_SINCE") except: - raise Http400('Bad If-Unmodified-Since header') + raise Http400("Bad If-Unmodified-Since header") @classmethod def validate_and_get_if_modified_since_header_as_tz_aware_datetime(cls, request): try: - return cls._validate_http_date_header(request, 'HTTP_IF_MODIFIED_SINCE') + return cls._validate_http_date_header(request, "HTTP_IF_MODIFIED_SINCE") except: - raise Http400('Bad If-Modified-Since header') + raise Http400("Bad If-Modified-Since header") @staticmethod def _validate_http_date_header(request, header_name): - timestamp = request.META.get(header_name, '') + timestamp = request.META.get(header_name, "") # According to RFC 7232, Http date should always be expressed in 'GMT'. Forcing its use makes this explicit - if not timestamp.endswith('GMT'): + if not timestamp.endswith("GMT"): raise Exception return parse_timestamp_string_to_tz_aware_datetime(timestamp) @@ -441,7 +468,7 @@ def _request_has_header(request, header_name): @staticmethod def _request_is_write_operation(request): - return request.method in ('POST', 'PUT', 'PATCH', 'DELETE') + return request.method in ("POST", "PUT", "PATCH", "DELETE") @staticmethod def request_is_create_operation(request): @@ -451,17 +478,23 @@ def request_is_create_operation(request): files to the dataset thus, not creating the dataset. This might not work for other datatypes out of the box. """ - return request.method in ('POST') and 'files' not in request.path + return request.method in ("POST") and "files" not in request.path @classmethod def check_if_unmodified_since(cls, request, obj): - if cls._request_is_write_operation(request) and \ - cls._request_has_header(request, 'HTTP_IF_UNMODIFIED_SINCE'): + if cls._request_is_write_operation(request) and cls._request_has_header( + request, "HTTP_IF_UNMODIFIED_SINCE" + ): - header_timestamp = cls._validate_and_get_if_unmodified_since_header_as_tz_aware_datetime(request) + header_timestamp = ( + cls._validate_and_get_if_unmodified_since_header_as_tz_aware_datetime(request) + ) if obj.modified_since(header_timestamp): - raise Http412('Resource has been modified since {0} (timezone: {1})'.format( - str(header_timestamp), timezone.get_default_timezone_name())) + raise Http412( + "Resource has been modified since {0} (timezone: {1})".format( + str(header_timestamp), timezone.get_default_timezone_name() + ) + ) @classmethod def set_if_modified_since_filter(cls, request, filter_obj): @@ -475,16 +508,18 @@ def set_if_modified_since_filter(cls, request, filter_obj): :param filter_obj :return: """ - if not cls._request_is_write_operation(request) and cls._request_has_header(request, 'HTTP_IF_MODIFIED_SINCE'): + if not cls._request_is_write_operation(request) and cls._request_has_header( + request, "HTTP_IF_MODIFIED_SINCE" + ): ts = cls.validate_and_get_if_modified_since_header_as_tz_aware_datetime(request) flter = Q(date_modified__gt=ts) | (Q(date_modified=None) & Q(date_created__gt=ts)) - if 'q_filters' in filter_obj: - filter_obj['q_filters'].append(flter) + if "q_filters" in filter_obj: + filter_obj["q_filters"].append(flter) else: - filter_obj['q_filters'] = [flter] + filter_obj["q_filters"] = [flter] @staticmethod def identifiers_to_ids(identifiers: List[any], params=None): @@ -493,16 +528,24 @@ def identifiers_to_ids(identifiers: List[any], params=None): do a query to get a list of pk's instead, since they will be used quite a few times. """ if not isinstance(identifiers, list): - raise Http400('Received identifiers is not a list') + raise Http400("Received identifiers is not a list") elif not identifiers: - _logger.info('Received empty list of identifiers. Aborting') - raise Http400('Received empty list of identifiers') + _logger.info("Received empty list of identifiers. Aborting") + raise Http400("Received empty list of identifiers") elif all(isinstance(x, int) for x in identifiers): return identifiers - if params in ['files', 'noparams']: - identifiers = [ id for id in File.objects.filter(identifier__in=identifiers).values_list('id', flat=True) ] + if params in ["files", "noparams"]: + identifiers = [ + id + for id in File.objects.filter(identifier__in=identifiers).values_list( + "id", flat=True + ) + ] else: - identifiers = [ id for id in cr.objects.filter(identifier__in=identifiers).values_list('id', flat=True) ] + identifiers = [ + id + for id in cr.objects.filter(identifier__in=identifiers).values_list("id", flat=True) + ] return identifiers diff --git a/src/metax_api/services/data_catalog_service.py b/src/metax_api/services/data_catalog_service.py index 72330430..6c3f333d 100755 --- a/src/metax_api/services/data_catalog_service.py +++ b/src/metax_api/services/data_catalog_service.py @@ -16,7 +16,6 @@ class DataCatalogService(ReferenceDataMixin): - @classmethod def validate_reference_data(cls, data_catalog, cache): """ @@ -33,50 +32,84 @@ def validate_reference_data(cls, data_catalog, cache): reference_data = cls.get_reference_data(cache) - refdata = reference_data['reference_data'] + refdata = reference_data["reference_data"] # ic(refdata.keys()) - orgdata = reference_data['organization_data'] + orgdata = reference_data["organization_data"] # ic(orgdata.keys()) errors = defaultdict(list) - for language in data_catalog.get('language', []): - ref_entry = cls.check_ref_data(refdata['language'], language['identifier'], - 'data_catalog_json.language.identifier', errors) + for language in data_catalog.get("language", []): + ref_entry = cls.check_ref_data( + refdata["language"], + language["identifier"], + "data_catalog_json.language.identifier", + errors, + ) if ref_entry: - label_field = 'title' - cls.populate_from_ref_data(ref_entry, language, label_field=label_field, add_in_scheme=False) + label_field = "title" + cls.populate_from_ref_data( + ref_entry, language, label_field=label_field, add_in_scheme=False + ) cls.remove_language_obj_irrelevant_titles(language, label_field) - for fos in data_catalog.get('field_of_science', []): - ref_entry = cls.check_ref_data(refdata['field_of_science'], fos['identifier'], - 'data_catalog_json.field_of_science.identifier', errors) + for fos in data_catalog.get("field_of_science", []): + ref_entry = cls.check_ref_data( + refdata["field_of_science"], + fos["identifier"], + "data_catalog_json.field_of_science.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, fos, label_field='pref_label', add_in_scheme=False) + cls.populate_from_ref_data( + ref_entry, fos, label_field="pref_label", add_in_scheme=False + ) - access_rights = data_catalog.get('access_rights', None) + access_rights = data_catalog.get("access_rights", None) if access_rights: - for access_type in access_rights.get('access_type', []): - ref_entry = cls.check_ref_data(refdata['access_type'], access_type['identifier'], - 'data_catalog_json.access_rights.access_type.identifier', errors) + for access_type in access_rights.get("access_type", []): + ref_entry = cls.check_ref_data( + refdata["access_type"], + access_type["identifier"], + "data_catalog_json.access_rights.access_type.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, access_type, label_field='pref_label', add_in_scheme=False) - - for license in access_rights.get('license', []): - ref_entry = cls.check_ref_data(refdata['license'], license['identifier'], - 'data_catalog_json.access_rights.license.identifier', errors) + cls.populate_from_ref_data( + ref_entry, + access_type, + label_field="pref_label", + add_in_scheme=False, + ) + + for license in access_rights.get("license", []): + ref_entry = cls.check_ref_data( + refdata["license"], + license["identifier"], + "data_catalog_json.access_rights.license.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, license, label_field='title', add_in_scheme=False) - - if 'has_rights_related_agent' in access_rights: - for agent in access_rights.get('has_rights_related_agent', []): - cls.process_org_obj_against_ref_data(orgdata['organization'], agent, - 'data_catalog_json.access_rights.has_rights_related_agent', - errors=errors) - - publisher = data_catalog.get('publisher', None) + cls.populate_from_ref_data( + ref_entry, license, label_field="title", add_in_scheme=False + ) + + if "has_rights_related_agent" in access_rights: + for agent in access_rights.get("has_rights_related_agent", []): + cls.process_org_obj_against_ref_data( + orgdata["organization"], + agent, + "data_catalog_json.access_rights.has_rights_related_agent", + errors=errors, + ) + + publisher = data_catalog.get("publisher", None) if publisher: - cls.process_org_obj_against_ref_data(orgdata['organization'], publisher, 'data_catalog_json.publisher', - errors=errors) + cls.process_org_obj_against_ref_data( + orgdata["organization"], + publisher, + "data_catalog_json.publisher", + errors=errors, + ) if errors: raise ValidationError(errors) @@ -86,4 +119,4 @@ def is_harvested(data_catalog): if not data_catalog: return False - return DataCatalog.objects.get(id=data_catalog).catalog_json['harvested'] + return DataCatalog.objects.get(id=data_catalog).catalog_json["harvested"] diff --git a/src/metax_api/services/datacite_service.py b/src/metax_api/services/datacite_service.py index aa24c362..144a8155 100755 --- a/src/metax_api/services/datacite_service.py +++ b/src/metax_api/services/datacite_service.py @@ -28,11 +28,11 @@ def convert_cr_to_datacite_cr_json(cr): - cr_json = {'research_dataset': cr.research_dataset} + cr_json = {"research_dataset": cr.research_dataset} if cr.date_created: - cr_json['date_created'] = datetime_to_str(cr.date_created) + cr_json["date_created"] = datetime_to_str(cr.date_created) if cr.preservation_identifier: - cr_json['preservation_identifier'] = cr.preservation_identifier + cr_json["preservation_identifier"] = cr.preservation_identifier return cr_json @@ -42,7 +42,7 @@ def DataciteService(*args, **kwargs): A factory for the Datacite service, which is capable of interacting with Datacite API and converting catalog records into datacite format. """ - if executing_test_case() or kwargs.pop('dummy', False): + if executing_test_case() or kwargs.pop("dummy", False): return _DataciteServiceDummy(*args, **kwargs) else: return _DataciteService(*args, **kwargs) @@ -60,27 +60,28 @@ class _DataciteService(CommonService): def __init__(self, settings=django_settings): if not isinstance(settings, dict): - if hasattr(settings, 'DATACITE'): + if hasattr(settings, "DATACITE"): settings = settings.DATACITE else: - raise Exception('Missing configuration from settings.py: DATACITE') + raise Exception("Missing configuration from settings.py: DATACITE") - if not settings.get('USERNAME', None): - raise Exception('Missing configuration from settings for DATACITE: USERNAME') - if not settings.get('PASSWORD', None): - raise Exception('Missing configuration from settings for DATACITE: PASSWORD') - if not settings.get('PREFIX', None): - raise Exception('Missing configuration from settings for DATACITE: PREFIX') + if not settings.get("USERNAME", None): + raise Exception("Missing configuration from settings for DATACITE: USERNAME") + if not settings.get("PASSWORD", None): + raise Exception("Missing configuration from settings for DATACITE: PASSWORD") + if not settings.get("PREFIX", None): + raise Exception("Missing configuration from settings for DATACITE: PREFIX") - self.user = settings['USERNAME'] - self.pw = settings['PASSWORD'] - self.url = settings['URL'] + self.user = settings["USERNAME"] + self.pw = settings["PASSWORD"] + self.url = settings["URL"] self.mds = DataCiteMDSClient( username=self.user, password=self.pw, - prefix=settings['PREFIX'], - url=self.url) + prefix=settings["PREFIX"], + url=self.url, + ) def create_doi_metadata(self, datacite_xml_metadata): """ @@ -124,87 +125,101 @@ def delete_draft_doi(self, doi): """ try: requests.delete( - '{0}/doi/{1}'.format(self.url, doi), - headers={'Content-Type': 'application/plain;charset=UTF-8'}, - auth=(self.user, self.pw) + "{0}/doi/{1}".format(self.url, doi), + headers={"Content-Type": "application/plain;charset=UTF-8"}, + auth=(self.user, self.pw), ) except Exception as e: - _logger.warning('Could not delete doi in draft state') + _logger.warning("Could not delete doi in draft state") _logger.warning(e) def get_validated_datacite_json(self, cr_json, is_strict, dummy_doi=False): if isinstance(cr_json, list): - raise DataciteException('Datacite conversion can only be done to individual datasets, not lists.') + raise DataciteException( + "Datacite conversion can only be done to individual datasets, not lists." + ) if not cr_json: - raise DataciteException("Catalog record containing research_dataset required to convert anything " - "to datacite format") + raise DataciteException( + "Catalog record containing research_dataset required to convert anything " + "to datacite format" + ) - rd = cr_json['research_dataset'] + rd = cr_json["research_dataset"] # Figure out main lang for the dataset, if applicable - if 'language' in rd and len(rd['language']) > 0: + if "language" in rd and len(rd["language"]) > 0: # used when trying to get most relevant name from langString when only one value is allowed. # note: language contains a three-letter language. we need a two-letter language, in order to # access the langString translations. this may not work as intended for some languages - lid = rd['language'][0]['identifier'] - start_idx = lid.rindex('/') + 1 - main_lang = lid[start_idx:start_idx + 2] + lid = rd["language"][0]["identifier"] + start_idx = lid.rindex("/") + 1 + main_lang = lid[start_idx : start_idx + 2] else: main_lang = None # Creators - if rd.get('creator', False): - creators = self._creators(rd['creator'], main_lang=main_lang) + if rd.get("creator", False): + creators = self._creators(rd["creator"], main_lang=main_lang) else: - raise DataciteException('Dataset does not have a creator (field: research_dataset.creator), which is a ' - 'required value for datacite format') + raise DataciteException( + "Dataset does not have a creator (field: research_dataset.creator), which is a " + "required value for datacite format" + ) # Titles - if rd.get('title', False): - titles = [{'lang': lang, 'title': title} for lang, title in rd['title'].items()] + if rd.get("title", False): + titles = [{"lang": lang, "title": title} for lang, title in rd["title"].items()] else: - raise DataciteException('Dataset does not have a title (field: research_dataset.title), which is ' - 'a required value for datacite format') + raise DataciteException( + "Dataset does not have a title (field: research_dataset.title), which is " + "a required value for datacite format" + ) # Publisher - if rd.get('publisher', False): - publisher = self._main_lang_or_default(rd['publisher']['name'], main_lang) + if rd.get("publisher", False): + publisher = self._main_lang_or_default(rd["publisher"]["name"], main_lang) elif is_strict: - raise DataciteException('Dataset does not have a publisher (field: research_dataset.publisher), which ' - 'is a required value for datacite format') + raise DataciteException( + "Dataset does not have a publisher (field: research_dataset.publisher), which " + "is a required value for datacite format" + ) else: - publisher = self._main_lang_or_default(rd['creator'][0]['name'], main_lang) + publisher = self._main_lang_or_default(rd["creator"][0]["name"], main_lang) # Publication year - if rd.get('issued', False): - publication_year = rd['issued'][0:4] + if rd.get("issued", False): + publication_year = rd["issued"][0:4] elif is_strict: - raise DataciteException('Dataset does not have a date of issuance (field: research_dataset.issued), which ' - 'is a required value for datacite format') + raise DataciteException( + "Dataset does not have a date of issuance (field: research_dataset.issued), which " + "is a required value for datacite format" + ) else: - publication_year = cr_json['date_created'][0:4] + publication_year = cr_json["date_created"][0:4] # Identifier - pref_id = rd['preferred_identifier'] - identifier = cr_json.get('preservation_identifier', None) or pref_id + pref_id = rd["preferred_identifier"] + identifier = cr_json.get("preservation_identifier", None) or pref_id is_metax_doi = is_metax_generated_doi_identifier(identifier) is_metax_urn = is_metax_generated_urn_identifier(identifier) is_remote_doi = is_remote_doi_identifier(identifier) if is_metax_doi or is_remote_doi: identifier_value = extract_doi_from_doi_identifier(identifier) - identifier_type = 'DOI' + identifier_type = "DOI" elif dummy_doi: - identifier_value = '10.0/%s' % identifier - identifier_type = 'DOI' + identifier_value = "10.0/%s" % identifier + identifier_type = "DOI" elif not is_strict and is_metax_urn: identifier_value = identifier - identifier_type = 'URN' + identifier_type = "URN" else: - raise DataciteException('Dataset does not have a valid preferred identifier (field: ' - 'research_dataset.preferred_identifier), which should contain a Metax ' - 'generated DOI, which is a required value for datacite format') + raise DataciteException( + "Dataset does not have a valid preferred identifier (field: " + "research_dataset.preferred_identifier), which should contain a Metax " + "generated DOI, which is a required value for datacite format" + ) """ Required fields, as specified by datacite: @@ -218,79 +233,97 @@ def get_validated_datacite_json(self, cr_json, is_strict, dummy_doi=False): """ datacite_json = { - 'identifier': { - 'identifier': identifier_value, - 'identifierType': identifier_type + "identifier": { + "identifier": identifier_value, + "identifierType": identifier_type, }, - 'creators': creators, - 'titles': titles, - 'publisher': publisher, - 'publicationYear': publication_year, - 'resourceType': { - 'resourceTypeGeneral': self._resource_type_general(rd) - } + "creators": creators, + "titles": titles, + "publisher": publisher, + "publicationYear": publication_year, + "resourceType": {"resourceTypeGeneral": self._resource_type_general(rd)}, } # Optional fields if is_metax_generated_urn_identifier(pref_id) and pref_id != identifier: - datacite_json['alternateIdentifiers'] = [{ - 'alternateIdentifier': pref_id, - 'alternateIdentifierType': 'URN' - }] - - if 'modified' in rd or 'available' in rd.get('access_rights', {}): - datacite_json['dates'] = [] - if 'modified' in rd: - datacite_json['dates'].append({'dateType': 'Updated', 'date': rd['modified']}) - if 'available' in rd.get('access_rights', {}): - datacite_json['dates'].append({'dateType': 'Available', 'date': rd['access_rights']['available']}) - - if 'keyword' in rd or 'field_of_science' in rd or 'theme' in rd: - datacite_json['subjects'] = [] - for kw in rd.get('keyword', []): - datacite_json['subjects'].append({'subject': kw}) - for fos in rd.get('field_of_science', []): - datacite_json['subjects'].extend(self._subjects(fos)) - for theme in rd.get('theme', []): - datacite_json['subjects'].extend(self._subjects(theme)) - - if 'total_files_byte_size' in rd: - datacite_json['sizes'] = [str(rd['total_files_byte_size'])] - - if rd.get('description', False): - datacite_json['descriptions'] = [ - {'lang': lang, 'description': desc, 'descriptionType': 'Abstract'} - for lang, desc in rd['description'].items() + datacite_json["alternateIdentifiers"] = [ + {"alternateIdentifier": pref_id, "alternateIdentifierType": "URN"} + ] + + if "modified" in rd or "available" in rd.get("access_rights", {}): + datacite_json["dates"] = [] + if "modified" in rd: + datacite_json["dates"].append({"dateType": "Updated", "date": rd["modified"]}) + if "available" in rd.get("access_rights", {}): + datacite_json["dates"].append( + {"dateType": "Available", "date": rd["access_rights"]["available"]} + ) + + if "keyword" in rd or "field_of_science" in rd or "theme" in rd: + datacite_json["subjects"] = [] + for kw in rd.get("keyword", []): + datacite_json["subjects"].append({"subject": kw}) + for fos in rd.get("field_of_science", []): + datacite_json["subjects"].extend(self._subjects(fos)) + for theme in rd.get("theme", []): + datacite_json["subjects"].extend(self._subjects(theme)) + + if "total_files_byte_size" in rd: + datacite_json["sizes"] = [str(rd["total_files_byte_size"])] + + if rd.get("description", False): + datacite_json["descriptions"] = [ + {"lang": lang, "description": desc, "descriptionType": "Abstract"} + for lang, desc in rd["description"].items() ] - if 'license' in rd['access_rights']: - datacite_json['rightsList'] = self._licenses(rd['access_rights']) - - if rd.get('language', False): - lid = rd['language'][0]['identifier'] - datacite_json['language'] = lid[lid.rindex('/') + 1:] - - if 'curator' in rd or 'contributor' in rd or 'creator' in rd or 'rights_holder' in rd or 'publisher' in rd: - datacite_json['contributors'] = [] - if 'curator' in rd: - datacite_json['contributors'].extend(self._contributors(rd['curator'], main_lang=main_lang)) - if 'contributor' in rd: - datacite_json['contributors'].extend(self._contributors(rd['contributor'], main_lang=main_lang)) - if 'creator' in rd: - datacite_json['contributors'].extend(self._contributors(rd['creator'], main_lang=main_lang)) - if 'rights_holder' in rd: - datacite_json['contributors'].extend(self._contributors(rd['rights_holder'], main_lang=main_lang)) - if 'publisher' in rd: - datacite_json['contributors'].extend(self._contributors(rd['publisher'], main_lang=main_lang)) - if 'spatial' in rd: - datacite_json['geoLocations'] = self._spatials(rd['spatial']) + if "license" in rd["access_rights"]: + datacite_json["rightsList"] = self._licenses(rd["access_rights"]) + + if rd.get("language", False): + lid = rd["language"][0]["identifier"] + datacite_json["language"] = lid[lid.rindex("/") + 1 :] + + if ( + "curator" in rd + or "contributor" in rd + or "creator" in rd + or "rights_holder" in rd + or "publisher" in rd + ): + datacite_json["contributors"] = [] + if "curator" in rd: + datacite_json["contributors"].extend( + self._contributors(rd["curator"], main_lang=main_lang) + ) + if "contributor" in rd: + datacite_json["contributors"].extend( + self._contributors(rd["contributor"], main_lang=main_lang) + ) + if "creator" in rd: + datacite_json["contributors"].extend( + self._contributors(rd["creator"], main_lang=main_lang) + ) + if "rights_holder" in rd: + datacite_json["contributors"].extend( + self._contributors(rd["rights_holder"], main_lang=main_lang) + ) + if "publisher" in rd: + datacite_json["contributors"].extend( + self._contributors(rd["publisher"], main_lang=main_lang) + ) + if "spatial" in rd: + datacite_json["geoLocations"] = self._spatials(rd["spatial"]) if is_strict: try: jsonschema.validate( datacite_json, - self.get_json_schema(join(dirname(dirname(__file__)), 'api/rest/base/schemas'), 'datacite_4.1') + self.get_json_schema( + join(dirname(dirname(__file__)), "api/rest/base/schemas"), + "datacite_4.1", + ), ) except Exception as e: _logger.error("Failed to validate catalog record against datacite schema") @@ -298,7 +331,9 @@ def get_validated_datacite_json(self, cr_json, is_strict, dummy_doi=False): return datacite_json - def convert_catalog_record_to_datacite_xml(self, cr_json, include_xml_declaration, is_strict, dummy_doi=False): + def convert_catalog_record_to_datacite_xml( + self, cr_json, include_xml_declaration, is_strict, dummy_doi=False + ): """ Convert dataset from catalog record data model to datacite json data model. Validate the json against datacite schema. On success, convert and return as XML. Raise exceptions on errors. @@ -311,7 +346,7 @@ def convert_catalog_record_to_datacite_xml(self, cr_json, include_xml_declaratio output_xml = datacite_schema41.tostring(datacite_json) if not include_xml_declaration: # the +1 is linebreak character - output_xml = output_xml[len("") + 1:] + output_xml = output_xml[len("") + 1 :] return output_xml @staticmethod @@ -322,7 +357,7 @@ def _main_lang_or_default(field, main_lang=None): Param 'field' may also be a standard str field, in which case the field's value is returned. """ if isinstance(field, dict): - for lang in (main_lang, 'en', 'fi', 'und'): + for lang in (main_lang, "en", "fi", "und"): try: return field[lang] except: @@ -341,11 +376,13 @@ def _resource_type_general(rd): def _creators(self, research_agents, main_lang): creators = [] for ra in research_agents: - cr = {'creatorName': self._main_lang_or_default(ra['name'], main_lang=main_lang)} - if 'identifier' in ra: - cr['nameIdentifiers'] = [{'nameIdentifier': ra['identifier'], 'nameIdentifierScheme': 'URI'}] - if 'member_of' in ra: - cr['affiliations'] = self._person_affiliations(ra, main_lang) + cr = {"creatorName": self._main_lang_or_default(ra["name"], main_lang=main_lang)} + if "identifier" in ra: + cr["nameIdentifiers"] = [ + {"nameIdentifier": ra["identifier"], "nameIdentifierScheme": "URI"} + ] + if "member_of" in ra: + cr["affiliations"] = self._person_affiliations(ra, main_lang) creators.append(cr) return creators @@ -355,23 +392,27 @@ def _contributors(self, research_agents, main_lang=None): contributors = [] for ra in research_agents: - if 'contributor_type' not in ra: + if "contributor_type" not in ra: continue - cr_base = {'contributorName': self._main_lang_or_default(ra['name'], main_lang=main_lang)} + cr_base = { + "contributorName": self._main_lang_or_default(ra["name"], main_lang=main_lang) + } - if 'identifier' in ra: - cr_base['nameIdentifiers'] = [{'nameIdentifier': ra['identifier'], 'nameIdentifierScheme': 'URI'}] + if "identifier" in ra: + cr_base["nameIdentifiers"] = [ + {"nameIdentifier": ra["identifier"], "nameIdentifierScheme": "URI"} + ] - if 'member_of' in ra: - cr_base['affiliations'] = self._person_affiliations(ra, main_lang) + if "member_of" in ra: + cr_base["affiliations"] = self._person_affiliations(ra, main_lang) - for ct in ra.get('contributor_type', []): + for ct in ra.get("contributor_type", []): # for example, extracts from initial value: # http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor # and produces value: Distributor cr = dict(cr_base) - cr['contributorType'] = ct['identifier'].split('contributor_type/code/')[-1] + cr["contributorType"] = ct["identifier"].split("contributor_type/code/")[-1] contributors.append(cr) return contributors @@ -384,34 +425,38 @@ def _person_affiliations(person, main_lang): # stuff the affiliation information in all its translations. the datacite spec is # not specific at all regarding this, it does not even care about the lang of the # the given value. - affs.append(person['member_of']['name'][main_lang]) + affs.append(person["member_of"]["name"][main_lang]) except KeyError: - for lang, name_translation in person['member_of']['name'].items(): + for lang, name_translation in person["member_of"]["name"].items(): affs.append(name_translation) return affs @staticmethod def _subjects(concept): subjects = [] - for lang in concept['pref_label'].keys(): + for lang in concept["pref_label"].keys(): item = {} - if lang in concept['pref_label']: - item['schemeURI'] = concept['in_scheme'] - item['subject'] = concept['pref_label'][lang] - item['lang'] = lang + if lang in concept["pref_label"]: + item["schemeURI"] = concept["in_scheme"] + item["subject"] = concept["pref_label"][lang] + item["lang"] = lang subjects.append(item) return subjects @staticmethod def _licenses(access_rights): licenses = [] - for license in access_rights['license']: - for lang in license['title'].keys(): - licenses.append({ - 'lang': lang, - 'rightsURI': license['license'] if 'license' in license else license['identifier'], - 'rights': license['title'][lang] - }) + for license in access_rights["license"]: + for lang in license["title"].keys(): + licenses.append( + { + "lang": lang, + "rightsURI": license["license"] + if "license" in license + else license["identifier"], + "rights": license["title"][lang], + } + ) return licenses @staticmethod @@ -420,29 +465,33 @@ def _spatials(spatials): for spatial in spatials: geo_location = {} - if 'geographic_name' in spatial: - geo_location['geoLocationPlace'] = spatial['geographic_name'] + if "geographic_name" in spatial: + geo_location["geoLocationPlace"] = spatial["geographic_name"] - for wkt in spatial.get('as_wkt', []): - if wkt.startswith('POINT'): - geo_location['geoLocationPoint'] = { - 'pointLongitude': float(re.search(r'POINT\((.*) ', wkt, re.IGNORECASE).group(1)), - 'pointLatitude': float(re.search(r' (.*)\)', wkt, re.IGNORECASE).group(1)), + for wkt in spatial.get("as_wkt", []): + if wkt.startswith("POINT"): + geo_location["geoLocationPoint"] = { + "pointLongitude": float( + re.search(r"POINT\((.*) ", wkt, re.IGNORECASE).group(1) + ), + "pointLatitude": float(re.search(r" (.*)\)", wkt, re.IGNORECASE).group(1)), } # only one point can be placed break - elif wkt.startswith('POLYGON'): - geo_location['geoLocationPolygon'] = { 'polygonPoints': [] } + elif wkt.startswith("POLYGON"): + geo_location["geoLocationPolygon"] = {"polygonPoints": []} # Split POLYGON in case it contains several polygon objects - for polygon in wkt.split('POLYGON')[1][2:-2].split('),('): - for point in polygon.split(','): - longitude, latitude = point.strip().split(' ') + for polygon in wkt.split("POLYGON")[1][2:-2].split("),("): + for point in polygon.split(","): + longitude, latitude = point.strip().split(" ") polygon_point = { - 'pointLongitude': float(longitude), - 'pointLatitude': float(latitude), + "pointLongitude": float(longitude), + "pointLatitude": float(latitude), } - geo_location['geoLocationPolygon']['polygonPoints'].append(polygon_point) + geo_location["geoLocationPolygon"]["polygonPoints"].append( + polygon_point + ) # Do not support for more than one polygon within one POLYGON value, for now break @@ -456,9 +505,9 @@ def _spatials(spatials): class _DataciteServiceDummy(_DataciteService): """ - A dummy Datacite service that doesn't connect to Datacite API but is able to convert catalog records to - datacite format. - """ + A dummy Datacite service that doesn't connect to Datacite API but is able to convert catalog records to + datacite format. + """ def __init__(self, settings=django_settings): pass diff --git a/src/metax_api/services/file_service.py b/src/metax_api/services/file_service.py index adb9b2e3..ad2d9617 100755 --- a/src/metax_api/services/file_service.py +++ b/src/metax_api/services/file_service.py @@ -40,13 +40,18 @@ -mechanic, so that the imports are not littered in several methods in FileService where they would otherwise be needed. """ + + def DirectorySerializer(*args, **kwargs): from metax_api.api.rest.base.serializers import DirectorySerializer as DS + DirectorySerializer = DS return DirectorySerializer(*args, **kwargs) + def FileSerializer(*args, **kwargs): from metax_api.api.rest.base.serializers import FileSerializer as FS + FileSerializer = FS return FileSerializer(*args, **kwargs) @@ -62,7 +67,7 @@ class FileService(CommonService, ReferenceDataMixin): @staticmethod def check_user_belongs_to_project(request, project_identifier): if project_identifier not in AuthService.get_user_projects(request): - raise Http403({ 'detail': [ 'You do not have access to this project.' ]}) + raise Http403({"detail": ["You do not have access to this project."]}) @classmethod def get_queryset_search_params(cls, request): @@ -75,16 +80,18 @@ def get_queryset_search_params(cls, request): queryset_search_params = {} - if request.query_params.get('project_identifier', False): - project = request.query_params['project_identifier'] + if request.query_params.get("project_identifier", False): + project = request.query_params["project_identifier"] if not request.user.is_service: cls.check_user_belongs_to_project(request, project) - queryset_search_params['project_identifier'] = project + queryset_search_params["project_identifier"] = project - if request.query_params.get('file_path', False): - if not request.query_params.get('project_identifier', False): - raise Http400('query parameter project_identifier is required when using file_path filter') - queryset_search_params['file_path__contains'] = request.query_params['file_path'] + if request.query_params.get("file_path", False): + if not request.query_params.get("project_identifier", False): + raise Http400( + "query parameter project_identifier is required when using file_path filter" + ) + queryset_search_params["file_path__contains"] = request.query_params["file_path"] return queryset_search_params @@ -96,47 +103,52 @@ def restore_files(cls, request, file_identifier_list): Only restores the files; does not touch datasets that might have been previously deprecated when a particular file was marked as removed. """ - _logger.info('Restoring files') + _logger.info("Restoring files") if not file_identifier_list: - _logger.info('Received file identifier list is empty - doing nothing') - return Response({ 'files_restored_count': 0 }, status=status.HTTP_200_OK) + _logger.info("Received file identifier list is empty - doing nothing") + return Response({"files_restored_count": 0}, status=status.HTTP_200_OK) for id in file_identifier_list: if not isinstance(id, str): - raise Http400({ - 'detail': [ - 'identifier values must be strings. found value \'%s\', which is of type %s' - % (id, type(id)) - ] - }) + raise Http400( + { + "detail": [ + "identifier values must be strings. found value '%s', which is of type %s" + % (id, type(id)) + ] + } + ) - _logger.info('Retrieving file details...') + _logger.info("Retrieving file details...") - file_details_list = File.objects_unfiltered \ - .filter(active=True, removed=True, identifier__in=file_identifier_list) \ - .values('id', 'identifier', 'file_path', 'project_identifier') + file_details_list = File.objects_unfiltered.filter( + active=True, removed=True, identifier__in=file_identifier_list + ).values("id", "identifier", "file_path", "project_identifier") if not len(file_details_list): - _logger.info('None of the requested files were found') + _logger.info("None of the requested files were found") raise Http404 elif len(file_details_list) < len(file_identifier_list): - _logger.info('Some of the requested files were not found. Aborting') + _logger.info("Some of the requested files were not found. Aborting") - found_pids = { f['identifier'] for f in file_details_list } - missing_identifiers = [ pid for pid in file_identifier_list if pid not in found_pids ] + found_pids = {f["identifier"] for f in file_details_list} + missing_identifiers = [pid for pid in file_identifier_list if pid not in found_pids] - raise Http400({ - 'detail': [ - 'not all requested file identifiers could be found. list of missing identifiers: %s' - % '\n'.join(missing_identifiers) - ] - }) + raise Http400( + { + "detail": [ + "not all requested file identifiers could be found. list of missing identifiers: %s" + % "\n".join(missing_identifiers) + ] + } + ) elif len(file_details_list) > len(file_identifier_list): - raise Http400({ - 'detail': [ - ''' + raise Http400( + { + "detail": [ + """ Found more files than were requested to be restored! Looks like there are some duplicates within the removed files (same identifier exists more than once). @@ -145,46 +157,51 @@ def restore_files(cls, request, file_identifier_list): attempting to restore. At this point, it is unclear which particular file of the many available ones should be restored. If feasible, in this situation it may be best to properly re-freeze those files entirely (so that new identifiers are generated). - ''' - ] - }) + """ + ] + } + ) else: # good case pass - _logger.info('Validating restore is targeting only one project...') + _logger.info("Validating restore is targeting only one project...") - projects = { f['project_identifier'] for f in file_details_list } + projects = {f["project_identifier"] for f in file_details_list} if len(projects) > 1: - raise Http400({ - 'detail': [ - 'restore operation should target one project at a time. the following projects were found: %s' - % ', '.join([ p for p in projects]) - ] - }) + raise Http400( + { + "detail": [ + "restore operation should target one project at a time. the following projects were found: %s" + % ", ".join([p for p in projects]) + ] + } + ) # note: sets do not support indexing. getting the first (only) item here project_identifier = next(iter(projects)) - _logger.info('Restoring files in project %s. Files to restore: %d' - % (project_identifier, len(file_identifier_list))) + _logger.info( + "Restoring files in project %s. Files to restore: %d" + % (project_identifier, len(file_identifier_list)) + ) # when files were deleted, any empty directories were deleted as well. check # and re-create directories, and assign new parent_directory_id to files being # restored as necessary. common_info = cls.update_common_info(request, return_only=True) - file_details_with_dirs = cls._create_directories_from_file_list(common_info, file_details_list) + file_details_with_dirs = cls._create_directories_from_file_list( + common_info, file_details_list + ) # note, the purpose of %%s: request.user.username is inserted into the query in cr.execute() as # a separate parameter, in order to properly escape it. update_statements = [ - '(%d, %%s, %d)' - % (f['id'], f['parent_directory']) - for f in file_details_with_dirs + "(%d, %%s, %d)" % (f["id"], f["parent_directory"]) for f in file_details_with_dirs ] - sql_restore_files = ''' + sql_restore_files = """ update metax_api_file as file set service_modified = results.service_modified, parent_directory_id = results.parent_directory_id, @@ -196,17 +213,22 @@ def restore_files(cls, request, file_identifier_list): %s ) as results(id, service_modified, parent_directory_id) where results.id = file.id; - ''' % ','.join(update_statements) + """ % ",".join( + update_statements + ) with connection.cursor() as cr: - cr.execute(sql_restore_files, [request.user.username for i in range(len(file_details_list))]) + cr.execute( + sql_restore_files, + [request.user.username for i in range(len(file_details_list))], + ) affected_rows = cr.rowcount - _logger.info('Restored %d files in project %s' % (affected_rows, project_identifier)) + _logger.info("Restored %d files in project %s" % (affected_rows, project_identifier)) cls.calculate_project_directory_byte_sizes_and_file_counts(project_identifier) - return Response({ 'restored_files_count': affected_rows }, status=status.HTTP_200_OK) + return Response({"restored_files_count": affected_rows}, status=status.HTTP_200_OK) @classmethod def get_identifiers(cls, identifiers, params, keysonly): @@ -222,14 +244,16 @@ def get_identifiers(cls, identifiers, params, keysonly): Parameter identifiers can be a list of pk's (integers), or file/dataset identifiers (strings). """ - _logger.info('Retrieving detailed list of %s' % params) + _logger.info("Retrieving detailed list of %s" % params) ids = cls.identifiers_to_ids(identifiers, params) if not ids: return Response([], status=status.HTTP_200_OK) - _logger.info('Searching return for the following %s (printing first 10):\n%s' - % (params, '\n'.join(str(id) for id in ids[:10]))) + _logger.info( + "Searching return for the following %s (printing first 10):\n%s" + % (params, "\n".join(str(id) for id in ids[:10])) + ) noparams = """ SELECT cr.identifier @@ -254,7 +278,9 @@ def get_identifiers(cls, identifiers, params, keysonly): ORDER BY f.id ASC; """ - files_keysonly = files.replace(", json_agg(cr.research_dataset->>'preferred_identifier')", "") + files_keysonly = files.replace( + ", json_agg(cr.research_dataset->>'preferred_identifier')", "" + ) datasets = """ SELECT cr.identifier, json_agg(f.identifier) @@ -272,22 +298,28 @@ def get_identifiers(cls, identifiers, params, keysonly): datasets_keysonly = datasets.replace(", json_agg(f.identifier)", "") if keysonly: - sql = {'files': files_keysonly, 'datasets': datasets_keysonly, 'noparams': noparams} + sql = { + "files": files_keysonly, + "datasets": datasets_keysonly, + "noparams": noparams, + } else: - sql = {'files': files, 'datasets': datasets, 'noparams': noparams} + sql = {"files": files, "datasets": datasets, "noparams": noparams} with connection.cursor() as cr: cr.execute(sql[params], [tuple(ids)]) if cr.rowcount == 0: preferred_identifiers = [] - _logger.info('No %s found for list of input identifiers' % params) + _logger.info("No %s found for list of input identifiers" % params) else: preferred_identifiers = cr.fetchall() - _logger.info('Found following %s:\n%s' % (params, preferred_identifiers)) + _logger.info("Found following %s:\n%s" % (params, preferred_identifiers)) if keysonly: - list_of_keys = [] # This has to be here, cr.fetchall() returns a list of tuples which dict - for tuples in preferred_identifiers: # can't parse like below when second item is empty + list_of_keys = ( + [] + ) # This has to be here, cr.fetchall() returns a list of tuples which dict + for tuples in preferred_identifiers: # can't parse like below when second item is empty list_of_keys.append(tuples[0]) return Response(list_of_keys, status=status.HTTP_200_OK) else: @@ -299,24 +331,28 @@ def destroy_single(cls, file): Mark a single file as removed. Marks related datasets deprecated, and deletes any empty directories above the file. """ - _logger.info('Begin delete file') + _logger.info("Begin delete file") deleted_files_count, project_identifier = cls._mark_files_as_deleted([file.id]) cls._delete_empy_dir_chain_above(file.parent_directory) cls.calculate_project_directory_byte_sizes_and_file_counts(file.project_identifier) cls._mark_datasets_as_deprecated([file.id]) - CallableService.add_post_request_callable(DelayedLog( - event='files_deleted', - files={ - 'project_identifier': file.project_identifier, - 'file_storage': file.file_storage.file_storage_json['identifier'], - 'file_count': deleted_files_count, - } - )) + CallableService.add_post_request_callable( + DelayedLog( + event="files_deleted", + files={ + "project_identifier": file.project_identifier, + "file_storage": file.file_storage.file_storage_json["identifier"], + "file_count": deleted_files_count, + }, + ) + ) - _logger.info('Marked %d files as deleted from project %s' % (deleted_files_count, project_identifier)) - return Response({ 'deleted_files_count': deleted_files_count }, status=status.HTTP_200_OK) + _logger.info( + "Marked %d files as deleted from project %s" % (deleted_files_count, project_identifier) + ) + return Response({"deleted_files_count": deleted_files_count}, status=status.HTTP_200_OK) @classmethod def destroy_bulk(cls, file_identifiers): @@ -334,9 +370,9 @@ def destroy_bulk(cls, file_identifiers): The method returns a http response with the number of deleted files in the body. """ - _logger.info('Begin bulk delete files') + _logger.info("Begin bulk delete files") - file_ids = cls.identifiers_to_ids(file_identifiers, 'noparams') + file_ids = cls.identifiers_to_ids(file_identifiers, "noparams") if not file_ids: raise Http404 @@ -348,17 +384,21 @@ def destroy_bulk(cls, file_identifiers): file = File.objects_unfiltered.get(pk=file_ids[0]) - CallableService.add_post_request_callable(DelayedLog( - event='files_deleted', - files={ - 'project_identifier': file.project_identifier, - 'file_storage': file.file_storage.file_storage_json['identifier'], - 'file_count': deleted_files_count, - } - )) + CallableService.add_post_request_callable( + DelayedLog( + event="files_deleted", + files={ + "project_identifier": file.project_identifier, + "file_storage": file.file_storage.file_storage_json["identifier"], + "file_count": deleted_files_count, + }, + ) + ) - _logger.info('Marked %d files as deleted from project %s' % (deleted_files_count, project_identifier)) - return Response({ 'deleted_files_count': deleted_files_count }, status=status.HTTP_200_OK) + _logger.info( + "Marked %d files as deleted from project %s" % (deleted_files_count, project_identifier) + ) + return Response({"deleted_files_count": deleted_files_count}, status=status.HTTP_200_OK) @classmethod def delete_project(cls, project_id): @@ -367,9 +407,14 @@ def delete_project(cls, project_id): This method is called by FileRPC """ - _logger.info('Begin to delete project from database...') + _logger.info("Begin to delete project from database...") - file_ids = [ id for id in File.objects.filter(project_identifier=project_id).values_list('id', flat=True) ] + file_ids = [ + id + for id in File.objects.filter(project_identifier=project_id).values_list( + "id", flat=True + ) + ] deleted_files_count = 0 @@ -379,34 +424,43 @@ def delete_project(cls, project_id): cls.calculate_project_directory_byte_sizes_and_file_counts(project_id) cls._mark_datasets_as_deprecated(file_ids) else: - _logger.info('Project %s contained no files' % project_id) + _logger.info("Project %s contained no files" % project_id) - _logger.info('Deleted project %s successfully. %d files deleted' % (project_id, deleted_files_count)) + _logger.info( + "Deleted project %s successfully. %d files deleted" % (project_id, deleted_files_count) + ) - return Response({ 'deleted_files_count': deleted_files_count }, status=status.HTTP_200_OK) + return Response({"deleted_files_count": deleted_files_count}, status=status.HTTP_200_OK) @staticmethod def _mark_files_as_deleted(file_ids): """ Mark files designated by file_ids as deleted. """ - _logger.info('Marking files as removed...') + _logger.info("Marking files as removed...") - sql_delete_files = ''' + sql_delete_files = """ update metax_api_file set removed = true, file_deleted = CURRENT_TIMESTAMP, date_modified = CURRENT_TIMESTAMP where active = true and removed = false - and id in %s''' + and id in %s""" - sql_select_related_projects = 'select distinct(project_identifier) from metax_api_file where id in %s' + sql_select_related_projects = ( + "select distinct(project_identifier) from metax_api_file where id in %s" + ) with connection.cursor() as cr: cr.execute(sql_select_related_projects, [tuple(file_ids)]) if cr.rowcount == 0: - raise Http400({ 'detail': ['no files found for given identifiers'] }) + raise Http400({"detail": ["no files found for given identifiers"]}) elif cr.rowcount > 1: - raise Http400({ 'project_identifier': [ - 'deleting files from more than one project in a single request is not allowed'] }) + raise Http400( + { + "project_identifier": [ + "deleting files from more than one project in a single request is not allowed" + ] + } + ) project_identifier = cr.fetchone()[0] cr.execute(sql_delete_files, [tuple(file_ids)]) @@ -420,17 +474,21 @@ def _find_and_delete_empty_directories(cls, project_identifier): Find and delete, if feasible, all empty directories in a project. The only dir found in the project without a parent dir, is considered to be the root. """ - _logger.info('Finding and deleting empty directory chains...') + _logger.info("Finding and deleting empty directory chains...") - root_dir = Directory.objects.filter(project_identifier=project_identifier, parent_directory_id=None) + root_dir = Directory.objects.filter( + project_identifier=project_identifier, parent_directory_id=None + ) - if root_dir.count() > 1: # pragma: no cover - raise ValidationError({ - 'detail': [ - 'found more than one root dir (directories without parents: %s. unable to proceed' % - ', '.join(dr.directory_path for dr in root_dir) - ] - }) + if root_dir.count() > 1: # pragma: no cover + raise ValidationError( + { + "detail": [ + "found more than one root dir (directories without parents: %s. unable to proceed" + % ", ".join(dr.directory_path for dr in root_dir) + ] + } + ) cls._delete_empy_directories(root_dir[0]) @@ -491,31 +549,46 @@ def _mark_datasets_as_deprecated(file_ids): Get all CatalogRecords which have files set to them from file_ids, and set their deprecated flag to True. Then, publish update-messages to rabbitmq. """ - _logger.info('Marking related datasets as deprecated...') + _logger.info("Marking related datasets as deprecated...") deprecated_records = [] current_time = get_tz_aware_now_without_micros() - records = CatalogRecord.objects \ - .filter(files__in=file_ids, deprecated=False) \ - .exclude(data_catalog__catalog_json__identifier=settings.PAS_DATA_CATALOG_IDENTIFIER) \ - .distinct('id') + records = ( + CatalogRecord.objects.filter(files__in=file_ids, deprecated=False) + .exclude(data_catalog__catalog_json__identifier=settings.PAS_DATA_CATALOG_IDENTIFIER) + .distinct("id") + ) for cr in records: cr.deprecate(current_time) deprecated_records.append(cr) if not deprecated_records: - _logger.info('Files were not associated with any datasets.') + _logger.info("Files were not associated with any datasets.") return from metax_api.models.catalog_record import RabbitMQPublishRecord + for cr in deprecated_records: - cr.add_post_request_callable(RabbitMQPublishRecord(cr, 'update')) + cr.add_post_request_callable(RabbitMQPublishRecord(cr, "update")) @classmethod - def get_directory_contents(cls, identifier=None, path=None, project_identifier=None, - recursive=False, max_depth=1, dirs_only=False, include_parent=False, cr_identifier=None, - not_cr_identifier=None, file_name=None, directory_name=None, paginate=None, request=None): + def get_directory_contents( + cls, + identifier=None, + path=None, + project_identifier=None, + recursive=False, + max_depth=1, + dirs_only=False, + include_parent=False, + cr_identifier=None, + not_cr_identifier=None, + file_name=None, + directory_name=None, + paginate=None, + request=None, + ): """ Get files and directories contained by a directory. @@ -559,21 +632,24 @@ def get_directory_contents(cls, identifier=None, path=None, project_identifier=N request: the web request object. """ - assert request is not None, 'kw parameter request must be specified' + assert request is not None, "kw parameter request must be specified" from metax_api.api.rest.base.serializers import LightDirectorySerializer # get targeted directory if identifier: try: - params = { 'id': int(identifier) } + params = {"id": int(identifier)} except ValueError: - params = { 'identifier': identifier } + params = {"identifier": identifier} else: if path and project_identifier: - params = { 'directory_path': path, 'project_identifier': project_identifier } - else: # pragma: no cover - raise ValidationError({'detail': ['no parameters to query by']}) + params = { + "directory_path": path, + "project_identifier": project_identifier, + } + else: # pragma: no cover + raise ValidationError({"detail": ["no parameters to query by"]}) try: fields = LightDirectorySerializer.ls_field_list() @@ -586,18 +662,20 @@ def get_directory_contents(cls, identifier=None, path=None, project_identifier=N if cr_identifier: cr_id, cr_directory_data = cls._get_cr_if_relevant(cr_identifier, directory, request) elif not_cr_identifier: - not_cr_id, cr_directory_data = cls._get_cr_if_relevant(not_cr_identifier, directory, request) + not_cr_id, cr_directory_data = cls._get_cr_if_relevant( + not_cr_identifier, directory, request + ) else: # generally browsing the directory - NOT in the context of a cr! check user permissions if not request.user.is_service: - cls.check_user_belongs_to_project(request, directory['project_identifier']) + cls.check_user_belongs_to_project(request, directory["project_identifier"]) cr_directory_data = {} # get list of field names to retrieve. note: by default all fields are retrieved directory_fields, file_fields = cls._get_requested_file_browsing_fields(request) - if cr_id and recursive and max_depth == '*' and not dirs_only: + if cr_id and recursive and max_depth == "*" and not dirs_only: # optimized for downloading full file list of an entire directory files = cls._get_directory_file_list_recursively_for_cr(directory, cr_id, file_fields) if paginate: @@ -606,12 +684,12 @@ def get_directory_contents(cls, identifier=None, path=None, project_identifier=N return files exclude_id = False - if (recursive or not_cr_id) and directory_fields and 'id' not in directory_fields: + if (recursive or not_cr_id) and directory_fields and "id" not in directory_fields: exclude_id = True - directory_fields.append('id') + directory_fields.append("id") contents = cls._get_directory_contents( - directory['id'], + directory["id"], recursive=recursive, max_depth=max_depth, dirs_only=dirs_only, @@ -622,14 +700,14 @@ def get_directory_contents(cls, identifier=None, path=None, project_identifier=N file_name=file_name, directory_name=directory_name, paginate=paginate, - request=request + request=request, ) def _remove_id(dirs): - for dir in dirs['directories']: - if dir.get('directories'): + for dir in dirs["directories"]: + if dir.get("directories"): _remove_id(dir) - dir.pop('id') + dir.pop("id") if exclude_id: _remove_id(contents) @@ -637,25 +715,28 @@ def _remove_id(dirs): if recursive: if paginate: if dirs_only: - contents['directories'], contents['files'] = cls.dp.paginate_directory_data(contents['directories'], - None, request) - del contents['files'] + ( + contents["directories"], + contents["files"], + ) = cls.dp.paginate_directory_data(contents["directories"], None, request) + del contents["files"] else: - dirs, files = cls.dp.paginate_directory_data(None, contents['files'], request) + dirs, files = cls.dp.paginate_directory_data(None, contents["files"], request) return cls.dp.get_paginated_response(files) if dirs_only: # taken care of the in the called methods. can return the result as is # as a directory tree pass else: - return contents.get('files', []) + return contents.get("files", []) if include_parent: contents.update(LightDirectorySerializer.serialize(directory)) if cls._include_total_byte_sizes_and_file_counts(cr_id, not_cr_id, directory_fields): - cls.retrieve_directory_byte_sizes_and_file_counts_for_cr(contents, not_cr_id, - directory_fields, cr_directory_data) + cls.retrieve_directory_byte_sizes_and_file_counts_for_cr( + contents, not_cr_id, directory_fields, cr_directory_data + ) if paginate: contents = cls.dp.get_paginated_response(contents) @@ -666,27 +747,30 @@ def _remove_id(dirs): def _get_cr_if_relevant(cls, cr_identifier, directory, request): # browsing in the context of a cr try: - cr_params = { 'id': int(cr_identifier) } + cr_params = {"id": int(cr_identifier)} except ValueError: - cr_params = { 'identifier': cr_identifier } + cr_params = {"identifier": cr_identifier} try: - cr = CatalogRecord.objects.only('id', '_directory_data', 'editor', 'user_created', 'research_dataset').\ - get(**cr_params) + cr = CatalogRecord.objects.only( + "id", "_directory_data", "user_created", "research_dataset" + ).get(**cr_params) except CatalogRecord.DoesNotExist: # raise 400 instead of 404, to distinguish from the error # 'directory not found', which raises a 404 - raise ValidationError({ - 'detail': [ 'CatalogRecord with identifier %s does not exist' % cr_identifier ] - }) + raise ValidationError( + {"detail": ["CatalogRecord with identifier %s does not exist" % cr_identifier]} + ) if not cr.authorized_to_see_catalog_record_files(request): - raise Http403({ - 'detail': [ - 'You do not have permission to see this information because the dataset access type is ' - 'not open and you are not the owner of the catalog record.' - ] - }) + raise Http403( + { + "detail": [ + "You do not have permission to see this information because the dataset access type is " + "not open and you are not the owner of the catalog record." + ] + } + ) cr_id = cr.id cr_directory_data = cr._directory_data or {} @@ -699,15 +783,19 @@ def _get_requested_file_browsing_fields(cls, request): Find out if only specific fields were requested to be returned, and return those fields for directories and files respectively. """ - from metax_api.api.rest.base.serializers import LightDirectorySerializer, LightFileSerializer + from metax_api.api.rest.base.serializers import ( + LightDirectorySerializer, + LightFileSerializer, + ) + directory_fields = [] file_fields = [] - if request.query_params.get('directory_fields', False): - directory_fields = request.query_params['directory_fields'].split(',') + if request.query_params.get("directory_fields", False): + directory_fields = request.query_params["directory_fields"].split(",") - if request.query_params.get('file_fields', False): - file_fields = request.query_params['file_fields'].split(',') + if request.query_params.get("file_fields", False): + file_fields = request.query_params["file_fields"].split(",") directory_fields = LightDirectorySerializer.ls_field_list(directory_fields) file_fields = LightFileSerializer.ls_field_list(file_fields) @@ -716,18 +804,19 @@ def _get_requested_file_browsing_fields(cls, request): @staticmethod def _get_directory_file_list_recursively_for_cr(directory, cr_id, file_fields): - ''' + """ Optimized for downloading full file list of an entire directory in a cr. Not a recursive method + no Model objects or normal serializers. - ''' + """ from metax_api.api.rest.base.serializers import LightFileSerializer - params = { 'project_identifier': directory['project_identifier'] } - if directory['directory_path'] == '/': + params = {"project_identifier": directory["project_identifier"]} + + if directory["directory_path"] == "/": # for root dir, simply omit file_path param to get all project files. pass else: - params['file_path__startswith'] = '%s/' % directory['directory_path'] + params["file_path__startswith"] = "%s/" % directory["directory_path"] files = CatalogRecord.objects.get(pk=cr_id).files.values(*file_fields).filter(**params) return LightFileSerializer.serialize(files) @@ -742,14 +831,27 @@ def _include_total_byte_sizes_and_file_counts(cr_id, not_cr_id, directory_fields if not directory_fields: # specific fields not specified -> all fields are returned return True - if 'byte_size' in directory_fields or 'file_count' in directory_fields: + if "byte_size" in directory_fields or "file_count" in directory_fields: return True return False @classmethod - def _get_directory_contents(cls, directory_id, request=None, recursive=False, max_depth=1, depth=0, dirs_only=False, - cr_id=None, not_cr_id=None, directory_fields=[], file_fields=[], file_name=None, directory_name=None, - paginate=None): + def _get_directory_contents( + cls, + directory_id, + request=None, + recursive=False, + max_depth=1, + depth=0, + dirs_only=False, + cr_id=None, + not_cr_id=None, + directory_fields=[], + file_fields=[], + file_name=None, + directory_name=None, + paginate=None, + ): """ Get files and directories contained by a directory. @@ -765,9 +867,9 @@ def _get_directory_contents(cls, directory_id, request=None, recursive=False, ma for directories and files respectively. """ - if recursive and max_depth != '*': + if recursive and max_depth != "*": if depth > max_depth: - raise MaxRecursionDepthExceeded('max depth is %d' % max_depth) + raise MaxRecursionDepthExceeded("max depth is %d" % max_depth) depth += 1 if cr_id or not_cr_id: @@ -781,17 +883,20 @@ def _get_directory_contents(cls, directory_id, request=None, recursive=False, ma recursive=recursive, dirs_only=dirs_only, directory_fields=directory_fields, - file_fields=file_fields + file_fields=file_fields, ) except Http404: if recursive: - return {'directories': []} + return {"directories": []} raise else: # browsing from ALL files, not cr specific - dirs = Directory.objects.filter(parent_directory_id=directory_id).order_by('directory_path').values( - *directory_fields) + dirs = ( + Directory.objects.filter(parent_directory_id=directory_id) + .order_by("directory_path") + .values(*directory_fields) + ) # icontains returns exception on None and with empty string does unnecessary db hits if directory_name: @@ -801,7 +906,11 @@ def _get_directory_contents(cls, directory_id, request=None, recursive=False, ma files = None else: - files = File.objects.filter(parent_directory_id=directory_id).order_by('file_path').values(*file_fields) + files = ( + File.objects.filter(parent_directory_id=directory_id) + .order_by("file_path") + .values(*file_fields) + ) if file_name: files = files.filter(file_name__icontains=file_name) @@ -809,19 +918,21 @@ def _get_directory_contents(cls, directory_id, request=None, recursive=False, ma dirs, files = cls.dp.paginate_directory_data(dirs, files, request) from metax_api.api.rest.base.serializers import LightDirectorySerializer - contents = { 'directories': LightDirectorySerializer.serialize(dirs) } + + contents = {"directories": LightDirectorySerializer.serialize(dirs)} if files or not dirs_only: # for normal file browsing (not with 'dirs_only'), the files-key should be present, # even if empty. from metax_api.api.rest.base.serializers import LightFileSerializer - contents['files'] = LightFileSerializer.serialize(files) + + contents["files"] = LightFileSerializer.serialize(files) if recursive: - for directory in contents['directories']: + for directory in contents["directories"]: try: sub_dir_contents = cls._get_directory_contents( - directory['id'], + directory["id"], recursive=recursive, max_depth=max_depth, depth=depth, @@ -833,28 +944,38 @@ def _get_directory_contents(cls, directory_id, request=None, recursive=False, ma file_name=file_name, directory_name=directory_name, paginate=paginate, - request=request + request=request, ) except MaxRecursionDepthExceeded: continue - directory['directories'] = sub_dir_contents['directories'] + directory["directories"] = sub_dir_contents["directories"] - if 'files' in sub_dir_contents: - contents['files'] += sub_dir_contents['files'] + if "files" in sub_dir_contents: + contents["files"] += sub_dir_contents["files"] return contents @classmethod - def _get_directory_contents_for_catalog_record(cls, directory_id, cr_id, not_cr_id, file_name, directory_name, - recursive, dirs_only=False, directory_fields=[], file_fields=[]): + def _get_directory_contents_for_catalog_record( + cls, + directory_id, + cr_id, + not_cr_id, + file_name, + directory_name, + recursive, + dirs_only=False, + directory_fields=[], + file_fields=[], + ): """ Browsing files in the context of a specific CR id. """ def _cr_belongin_to_directory(id): if recursive and not dirs_only: - return Directory.objects.filter(parent_directory_id=directory_id).values('id') + return Directory.objects.filter(parent_directory_id=directory_id).values("id") # select dirs which are contained by the directory, # AND which contain files belonging to the cr <-> files m2m relation table, @@ -876,13 +997,19 @@ def _cr_belongin_to_directory(id): for field in directory_fields: if field in allowed_fields: - directory_fields_sql.append('d.' + field) - elif 'parent_directory__' in field and field.split('parent_directory__')[1] in allowed_fields: - directory_fields_sql.append(field.replace('parent_directory__', 'parent_d.')) - directory_fields_string_sql = ', '.join(directory_fields_sql) - - dir_name_sql = '' if not directory_name or not_cr_id else \ - "AND d.directory_name LIKE ('%%' || %s || '%%')" + directory_fields_sql.append("d." + field) + elif ( + "parent_directory__" in field + and field.split("parent_directory__")[1] in allowed_fields + ): + directory_fields_sql.append(field.replace("parent_directory__", "parent_d.")) + directory_fields_string_sql = ", ".join(directory_fields_sql) + + dir_name_sql = ( + "" + if not directory_name or not_cr_id + else "AND d.directory_name LIKE ('%%' || %s || '%%')" + ) sql_select_dirs_for_cr = """ SELECT {} @@ -903,9 +1030,14 @@ def _cr_belongin_to_directory(id): ORDER BY d.directory_path """ with connection.cursor() as cr: - sql_select_dirs_for_cr = sql_select_dirs_for_cr.format(directory_fields_string_sql, dir_name_sql) - sql_params = [directory_id, directory_name, id] if directory_name and not not_cr_id \ + sql_select_dirs_for_cr = sql_select_dirs_for_cr.format( + directory_fields_string_sql, dir_name_sql + ) + sql_params = ( + [directory_id, directory_name, id] + if directory_name and not not_cr_id else [directory_id, id] + ) cr.execute(sql_select_dirs_for_cr, sql_params) dirs = [dict(zip(directory_fields, row)) for row in cr.fetchall()] @@ -915,23 +1047,37 @@ def _cr_belongin_to_directory(id): if cr_id: dirs = _cr_belongin_to_directory(cr_id) - files = None if dirs_only else File.objects \ - .filter(record__pk=cr_id, parent_directory=directory_id).order_by('file_path').values(*file_fields) + files = ( + None + if dirs_only + else File.objects.filter(record__pk=cr_id, parent_directory=directory_id) + .order_by("file_path") + .values(*file_fields) + ) elif not_cr_id: dirs = _cr_belongin_to_directory(not_cr_id) if dirs_only or not recursive: - dirs = Directory.objects.filter(parent_directory=directory_id).exclude( - id__in=[dir['id'] for dir in dirs]).values(*directory_fields) + dirs = ( + Directory.objects.filter(parent_directory=directory_id) + .exclude(id__in=[dir["id"] for dir in dirs]) + .values(*directory_fields) + ) if directory_name: dirs = dirs.filter(directory_name__icontains=directory_name) if directory_name: dirs = dirs.filter(directory_name__icontains=directory_name) - files = None if dirs_only else File.objects.exclude(record__pk=not_cr_id) \ - .filter(parent_directory=directory_id).order_by('file_path').values(*file_fields) + files = ( + None + if dirs_only + else File.objects.exclude(record__pk=not_cr_id) + .filter(parent_directory=directory_id) + .order_by("file_path") + .values(*file_fields) + ) if not dirs and not files: # for this specific version of the record, the requested directory either @@ -944,8 +1090,9 @@ def _cr_belongin_to_directory(id): return dirs, files @classmethod - def retrieve_directory_byte_sizes_and_file_counts_for_cr(cls, directory, not_cr_id=None, - directory_fields=[], cr_directory_data={}): + def retrieve_directory_byte_sizes_and_file_counts_for_cr( + cls, directory, not_cr_id=None, directory_fields=[], cr_directory_data={} + ): """ Retrieve total byte size and file counts of a directory, sub-directories included, in the context of a specific catalog record. @@ -954,32 +1101,35 @@ def retrieve_directory_byte_sizes_and_file_counts_for_cr(cls, directory, not_cr_ if not directory_fields: BYTE_SIZE = FILE_COUNT = True else: - BYTE_SIZE = 'byte_size' in directory_fields - FILE_COUNT = 'file_count' in directory_fields + BYTE_SIZE = "byte_size" in directory_fields + FILE_COUNT = "file_count" in directory_fields - if len(directory.get('directories', [])): - for sub_dir in directory.get('directories', []): - cls.retrieve_directory_byte_sizes_and_file_counts_for_cr(sub_dir, not_cr_id, - directory_fields, cr_directory_data) + if len(directory.get("directories", [])): + for sub_dir in directory.get("directories", []): + cls.retrieve_directory_byte_sizes_and_file_counts_for_cr( + sub_dir, not_cr_id, directory_fields, cr_directory_data + ) - if 'id' in directory: + if "id" in directory: # bottom dir - retrieve total byte_size and file_count for this cr - current_dir = cr_directory_data.get(str(directory['id']), [0, 0]) + current_dir = cr_directory_data.get(str(directory["id"]), [0, 0]) if not_cr_id: - dr_total = Directory.objects.values('id', 'byte_size', 'file_count').get(id=directory['id']) + dr_total = Directory.objects.values("id", "byte_size", "file_count").get( + id=directory["id"] + ) if BYTE_SIZE: if not_cr_id: - directory['byte_size'] = dr_total['byte_size'] - current_dir[0] + directory["byte_size"] = dr_total["byte_size"] - current_dir[0] else: - directory['byte_size'] = current_dir[0] + directory["byte_size"] = current_dir[0] if FILE_COUNT: if not_cr_id: - directory['file_count'] = dr_total['file_count'] - current_dir[1] + directory["file_count"] = dr_total["file_count"] - current_dir[1] else: - directory['file_count'] = current_dir[1] + directory["file_count"] = current_dir[1] @classmethod def get_project_root_directory(cls, project_identifier): @@ -987,6 +1137,7 @@ def get_project_root_directory(cls, project_identifier): Return root directory for a project, with its child directories and files. """ from metax_api.api.rest.base.serializers import LightDirectorySerializer + directory_fields = LightDirectorySerializer.ls_field_list() try: root_dir = Directory.objects.values(*directory_fields).get( @@ -994,12 +1145,14 @@ def get_project_root_directory(cls, project_identifier): ) except Directory.DoesNotExist: raise Http404 - except Directory.MultipleObjectsReturned: # pragma: no cover + except Directory.MultipleObjectsReturned: # pragma: no cover raise Exception( - 'Directory.MultipleObjectsReturned when looking for root directory. This should never happen' + "Directory.MultipleObjectsReturned when looking for root directory. This should never happen" ) root_dir_json = LightDirectorySerializer.serialize(root_dir) - root_dir_json.update(cls._get_directory_contents(root_dir['id'], directory_fields=directory_fields)) + root_dir_json.update( + cls._get_directory_contents(root_dir["id"], directory_fields=directory_fields) + ) return root_dir_json @classmethod @@ -1008,52 +1161,61 @@ def _create_single(cls, common_info, initial_data, serializer_class, **kwargs): Override the original _create_single from CommonService to also create directories, and setting them as parent_directory to approriate dirs and the file, before creating. """ - _logger.info('Begin create single file') + _logger.info("Begin create single file") cls._check_errors_before_creating_dirs([initial_data]) # the same initial data as received, except it has parent_directory set also - initial_data_with_dirs = cls._create_directories_from_file_list(common_info, [initial_data], **kwargs) + initial_data_with_dirs = cls._create_directories_from_file_list( + common_info, [initial_data], **kwargs + ) res = super(FileService, cls)._create_single( - common_info, initial_data_with_dirs[0], serializer_class, **kwargs) + common_info, initial_data_with_dirs[0], serializer_class, **kwargs + ) - cls.calculate_project_directory_byte_sizes_and_file_counts(initial_data['project_identifier']) + cls.calculate_project_directory_byte_sizes_and_file_counts( + initial_data["project_identifier"] + ) - CallableService.add_post_request_callable(DelayedLog( - event='files_created', - user_id=initial_data.get('user_created', res[0]['service_created']), - files={ - 'project_identifier': initial_data['project_identifier'], - 'file_storage': str(initial_data['file_storage']), - 'file_count': 1, - }, - )) + CallableService.add_post_request_callable( + DelayedLog( + event="files_created", + user_id=initial_data.get("user_created", res[0]["service_created"]), + files={ + "project_identifier": initial_data["project_identifier"], + "file_storage": str(initial_data["file_storage"]), + "file_count": 1, + }, + ) + ) - _logger.info('Created 1 new files') + _logger.info("Created 1 new files") return res @classmethod def check_allowed_projects(cls, request): - allowed_projects = CommonService.get_list_query_param(request, 'allowed_projects') + allowed_projects = CommonService.get_list_query_param(request, "allowed_projects") if allowed_projects is not None: if not isinstance(request.data, list): - raise Http400({ 'detail': [ 'request message body must be a single json object' ] }) + raise Http400({"detail": ["request message body must be a single json object"]}) try: - file_ids = [f['identifier'] for f in request.data] + file_ids = [f["identifier"] for f in request.data] except KeyError: - raise Http400({ 'detail': [ 'File identifier is missing' ] }) + raise Http400({"detail": ["File identifier is missing"]}) - project_ids = [ pid for pid in File.objects - .filter(identifier__in=file_ids) - .values_list('project_identifier', flat=True) - .distinct('project_identifier') ] + project_ids = [ + pid + for pid in File.objects.filter(identifier__in=file_ids) + .values_list("project_identifier", flat=True) + .distinct("project_identifier") + ] if not all(pid in allowed_projects for pid in project_ids): - raise Http403({ 'detail': [ 'You do not have permission to update this file' ] }) + raise Http403({"detail": ["You do not have permission to update this file"]}) @classmethod def _create_bulk(cls, common_info, initial_data_list, results, serializer_class, **kwargs): @@ -1061,50 +1223,56 @@ def _create_bulk(cls, common_info, initial_data_list, results, serializer_class, Override the original _create_bulk from CommonService to also create directories, and setting them as parent_directory to approriate files, before creating the files. """ - _logger.info('Begin bulk create files') + _logger.info("Begin bulk create files") cls._check_errors_before_creating_dirs(initial_data_list) - file_list_with_dirs = cls._create_directories_from_file_list(common_info, initial_data_list, **kwargs) + file_list_with_dirs = cls._create_directories_from_file_list( + common_info, initial_data_list, **kwargs + ) - _logger.info('Creating files...') + _logger.info("Creating files...") - cls._create_files( - common_info, file_list_with_dirs, results, serializer_class, **kwargs) + cls._create_files(common_info, file_list_with_dirs, results, serializer_class, **kwargs) - cls.calculate_project_directory_byte_sizes_and_file_counts(initial_data_list[0]['project_identifier']) + cls.calculate_project_directory_byte_sizes_and_file_counts( + initial_data_list[0]["project_identifier"] + ) - CallableService.add_post_request_callable(DelayedLog( - event='files_created', - user_id=initial_data_list[0].get('user_created', common_info['service_created']), - files={ - 'project_identifier': initial_data_list[0]['project_identifier'], - 'file_storage': str(initial_data_list[0]['file_storage']), - 'file_count': len(results.get('success', [])), - } - )) + CallableService.add_post_request_callable( + DelayedLog( + event="files_created", + user_id=initial_data_list[0].get("user_created", common_info["service_created"]), + files={ + "project_identifier": initial_data_list[0]["project_identifier"], + "file_storage": str(initial_data_list[0]["file_storage"]), + "file_count": len(results.get("success", [])), + }, + ) + ) - _logger.info('Created %d new files' % len(results.get('success', []))) + _logger.info("Created %d new files" % len(results.get("success", []))) @classmethod def _create_files(cls, common_info, initial_data_list, results, serializer_class, **kwargs): """ The actual part where the list is iterated and objects validated, and created. """ - project_identifier = initial_data_list[0]['project_identifier'] + project_identifier = initial_data_list[0]["project_identifier"] # pre-fetch all file_paths in the project, to spare an individual db fetch for each file # in serializer.save(), where they otherwise would check for path presence. - project_file_paths = File.objects.filter( - project_identifier=project_identifier).values_list('file_path', flat=True) + project_file_paths = File.objects.filter(project_identifier=project_identifier).values_list( + "file_path", flat=True + ) project_file_paths = set(project_file_paths) - project_dir_paths = set(dirname(f['file_path']) for f in initial_data_list) + project_dir_paths = set(dirname(f["file_path"]) for f in initial_data_list) - project_dir_data_list = Directory.objects \ - .filter(project_identifier=project_identifier, directory_path__in=project_dir_paths) \ - .values_list('id', 'identifier') + project_dir_data_list = Directory.objects.filter( + project_identifier=project_identifier, directory_path__in=project_dir_paths + ).values_list("id", "identifier") - project_dir_data = { dr[0]: dr[1] for dr in project_dir_data_list } + project_dir_data = {dr[0]: dr[1] for dr in project_dir_data_list} file_storage_id = None file_storage_identifier = None @@ -1114,34 +1282,34 @@ def to_model_format(entry, common_info): """ Format that is inserted into db. """ - del entry['checksum'] - entry['file_storage_id'] = entry['file_storage'] - del entry['file_storage'] - entry['parent_directory_id'] = entry['parent_directory'] - del entry['parent_directory'] - entry.update(**common_info) # add date_created, service_created etc fields + del entry["checksum"] + entry["file_storage_id"] = entry["file_storage"] + del entry["file_storage"] + entry["parent_directory_id"] = entry["parent_directory"] + del entry["parent_directory"] + entry.update(**common_info) # add date_created, service_created etc fields def to_repr(entry, common_info, project_dir_data, file_storage_identifier): """ Format that is returned in the response. """ - entry['file_storage'] = { - 'id': entry['file_storage_id'], - 'identifier': file_storage_identifier, + entry["file_storage"] = { + "id": entry["file_storage_id"], + "identifier": file_storage_identifier, } - entry['parent_directory'] = { - 'id': entry['parent_directory_id'], - 'identifier': project_dir_data[entry['parent_directory_id']], + entry["parent_directory"] = { + "id": entry["parent_directory_id"], + "identifier": project_dir_data[entry["parent_directory_id"]], } - del entry['file_storage_id'] - del entry['parent_directory_id'] + del entry["file_storage_id"] + del entry["parent_directory_id"] for field in common_info.keys(): # cast datetime objects into strings entry[field] = str(entry[field]) - entry['checksum'] = serializer_class.form_checksum(entry) + entry["checksum"] = serializer_class.form_checksum(entry) if DEBUG: start = time() @@ -1151,11 +1319,11 @@ def to_repr(entry, common_info, project_dir_data, file_storage_identifier): if file_storage_id: # saves a fetch to db in serializer.is_valid(), once file_storage_id has been retrieved # for one of the files. - row['file_storage'] = file_storage_id + row["file_storage"] = file_storage_id serializer = serializer_class(data=row, **kwargs) - if row['file_path'] not in project_file_paths: + if row["file_path"] not in project_file_paths: # saves a fetch to db in serializer.is_valid() serializer.file_path_checked = True else: @@ -1168,27 +1336,35 @@ def to_repr(entry, common_info, project_dir_data, file_storage_identifier): try: serializer.is_valid(raise_exception=True) except Exception as e: - if CommonService.get_boolean_query_param(kwargs['context']['request'], 'ignore_already_exists_errors'): + if CommonService.get_boolean_query_param( + kwargs["context"]["request"], "ignore_already_exists_errors" + ): if cls._error_is_already_exists(e): # add only a minuscule response informing of the situation... - results['success'].append({ 'object': { - 'identifier': row['identifier'], 'detail': ['already exists'] } - }) + results["success"].append( + { + "object": { + "identifier": row["identifier"], + "detail": ["already exists"], + } + } + ) continue cls._append_error(results, serializer, e) else: entry = serializer.initial_data to_model_format(entry, common_info) entries.append(File(**entry)) - file_storage_id = entry['file_storage_id'] # re-used for following loops + file_storage_id = entry["file_storage_id"] # re-used for following loops if file_storage_identifier is None: # re-used for following loops - file_storage_identifier = FileStorage.objects \ - .get(pk=file_storage_id).file_storage_json['identifier'] + file_storage_identifier = FileStorage.objects.get( + pk=file_storage_id + ).file_storage_json["identifier"] to_repr(entry, common_info, project_dir_data, file_storage_identifier) - results['success'].append({ 'object': entry }) + results["success"].append({"object": entry}) if i % 1000 == 0: # pros and cons of Model.objects.bulk_create(): @@ -1216,17 +1392,22 @@ def to_repr(entry, common_info, project_dir_data, file_storage_identifier): if DEBUG: end = time() - _logger.debug('processed %d files... (%.3f seconds per batch)' % (i, end - start)) + _logger.debug( + "processed %d files... (%.3f seconds per batch)" % (i, end - start) + ) start = time() if entries: - _logger.debug('a final dose of %d records still left to bulk_create...' % len(entries)) + _logger.debug("a final dose of %d records still left to bulk_create..." % len(entries)) File.objects.bulk_create(entries) - _logger.debug('done!') + _logger.debug("done!") if DEBUG: end = time() - _logger.debug('total time for inserting %d files: %d seconds' % (len(initial_data_list), (end - start))) + _logger.debug( + "total time for inserting %d files: %d seconds" + % (len(initial_data_list), (end - start)) + ) @staticmethod def _error_is_already_exists(e): @@ -1234,9 +1415,9 @@ def _error_is_already_exists(e): Check if the error 'identifier already exists' was raised. There may have been other errors included, but they may be a symptom of the record already existing, so we don't care about them. """ - if hasattr(e, 'detail'): + if hasattr(e, "detail"): for field_name, errors in e.detail.items(): - if field_name == 'identifier' and 'already exists' in errors[0]: + if field_name == "identifier" and "already exists" in errors[0]: return True return False @@ -1251,29 +1432,43 @@ def _check_errors_before_creating_dirs(initial_data_list): """ for row in initial_data_list: # ic(row) - if 'file_path' not in row: - raise Http400({ - 'file_path': ['file_path is a required parameter (file id: %s)' % row['identifier']] - }) + if "file_path" not in row: + raise Http400( + { + "file_path": [ + "file_path is a required parameter (file id: %s)" % row["identifier"] + ] + } + ) else: - if row['file_path'][0] != '/': - raise Http400({ - 'file_path': [ - "file path should start with '/' to point to the root. Now '%s'" % row['file_path'] + if row["file_path"][0] != "/": + raise Http400( + { + "file_path": [ + "file path should start with '/' to point to the root. Now '%s'" + % row["file_path"] + ] + } + ) + + if "project_identifier" not in row: + raise Http400( + { + "project_identifier": [ + "project_identifier is a required parameter (file id: %s)" + % row["identifier"] ] - }) + } + ) - if 'project_identifier' not in row: - raise Http400({ - 'project_identifier': [ - 'project_identifier is a required parameter (file id: %s)' % row['identifier'] + if len(set(f["project_identifier"] for f in initial_data_list)) > 1: + raise Http400( + { + "project_identifier": [ + "creating files for multiple projects in one request is not permitted." ] - }) - - if len(set(f['project_identifier'] for f in initial_data_list)) > 1: - raise Http400({ - 'project_identifier': ['creating files for multiple projects in one request is not permitted.'] - }) + } + ) @classmethod def _create_directories_from_file_list(cls, common_info, initial_data_list, **kwargs): @@ -1282,20 +1477,24 @@ def _create_directories_from_file_list(cls, common_info, initial_data_list, **kw as separate entities in the request, so they have to be created based on the paths in the list of files. """ - _logger.info('Checking and creating file hierarchy...') + _logger.info("Checking and creating file hierarchy...") - project_identifier = initial_data_list[0]['project_identifier'] - sorted_data = sorted(initial_data_list, key=lambda row: row['file_path']) - received_dir_paths = sorted(set(dirname(f['file_path']) for f in sorted_data)) + project_identifier = initial_data_list[0]["project_identifier"] + sorted_data = sorted(initial_data_list, key=lambda row: row["file_path"]) + received_dir_paths = sorted(set(dirname(f["file_path"]) for f in sorted_data)) - new_dir_paths, existing_dirs = cls._get_new_and_existing_dirs(received_dir_paths, project_identifier) + new_dir_paths, existing_dirs = cls._get_new_and_existing_dirs( + received_dir_paths, project_identifier + ) if new_dir_paths: - cls._create_directories(common_info, existing_dirs, new_dir_paths, project_identifier, **kwargs) + cls._create_directories( + common_info, existing_dirs, new_dir_paths, project_identifier, **kwargs + ) cls._assign_parents_to_files(existing_dirs, sorted_data) - _logger.info('Directory hierarchy in place') + _logger.info("Directory hierarchy in place") return sorted_data @classmethod @@ -1308,15 +1507,17 @@ def _get_new_and_existing_dirs(cls, received_dir_paths, project_identifier): # get existing dirs as a dict of { 'directory_path': 'id' } existing_dirs = dict( - (dr.directory_path, dr.id) for dr in - Directory.objects.filter( + (dr.directory_path, dr.id) + for dr in Directory.objects.filter( directory_path__in=received_dir_paths, - project_identifier=project_identifier - ).order_by('directory_path') + project_identifier=project_identifier, + ).order_by("directory_path") ) - new_dir_paths = [ path for path in received_dir_paths if path not in existing_dirs ] - _logger.info('Found %d existing, %d new directory paths' % (len(existing_dirs), len(new_dir_paths))) + new_dir_paths = [path for path in received_dir_paths if path not in existing_dirs] + _logger.info( + "Found %d existing, %d new directory paths" % (len(existing_dirs), len(new_dir_paths)) + ) return new_dir_paths, existing_dirs def _get_unique_dir_paths(received_dir_paths): @@ -1325,11 +1526,11 @@ def _get_unique_dir_paths(received_dir_paths): need to be weeded out from the list of received dir paths, to get all required directories that need to be created. """ - all_paths = { '/': True } + all_paths = {"/": True} for path in received_dir_paths: parent_path = dirname(path) - while parent_path != '/': + while parent_path != "/": if parent_path not in all_paths: all_paths[parent_path] = True parent_path = dirname(parent_path) @@ -1338,7 +1539,9 @@ def _get_unique_dir_paths(received_dir_paths): return sorted(all_paths.keys()) @classmethod - def _create_directories(cls, common_info, existing_dirs, new_dir_paths, project_identifier, **kwargs): + def _create_directories( + cls, common_info, existing_dirs, new_dir_paths, project_identifier, **kwargs + ): """ Create to db directory hierarchy from directories extracted from the received file list. @@ -1348,29 +1551,31 @@ def _create_directories(cls, common_info, existing_dirs, new_dir_paths, project_ It is possible, that no new directories are created at all, when appending files to an existing dir. """ - _logger.info('Creating directories...') + _logger.info("Creating directories...") python_process_pid = str(getpid()) for i, path in enumerate(new_dir_paths): directory = { - 'directory_path': path, - 'directory_name': basename(path) if path != '/' else '/', + "directory_path": path, + "directory_name": basename(path) if path != "/" else "/", # identifier: uuid3 as hex, using as salt time in ms, idx of loop, and python process id - 'identifier': uuid3(UUID_NAMESPACE_DNS, '%d%d%s' - % (int(round(time() * 1000)), i, python_process_pid)).hex, - 'project_identifier': project_identifier + "identifier": uuid3( + UUID_NAMESPACE_DNS, + "%d%d%s" % (int(round(time() * 1000)), i, python_process_pid), + ).hex, + "project_identifier": project_identifier, } directory.update(common_info) - if path != '/': + if path != "/": cls._find_parent_dir_from_previously_created_dirs(directory, existing_dirs) serializer = DirectorySerializer(data=directory, **kwargs) serializer.is_valid(raise_exception=True) serializer.save() - existing_dirs[serializer.data['directory_path']] = serializer.data['id'] + existing_dirs[serializer.data["directory_path"]] = serializer.data["id"] @classmethod def _assign_parents_to_files(cls, existing_dirs, sorted_data): @@ -1381,10 +1586,10 @@ def _assign_parents_to_files(cls, existing_dirs, sorted_data): Assigning parent_directory is not allowed for requestors, so all existing parent_directories in the received files are purged. """ - _logger.info('Assigning parent directories to files...') + _logger.info("Assigning parent directories to files...") for row in sorted_data: - row.pop('parent_directory', None) + row.pop("parent_directory", None) for row in sorted_data: cls._find_parent_dir_from_previously_created_dirs(row, existing_dirs) @@ -1399,19 +1604,20 @@ def _find_parent_dir_from_previously_created_dirs(node, existing_dirs): Find the approriate directory id to use as parent_directory, using the node's dirname(file_path) or dirname(directory_path) as key. """ - node_path = node.get('file_path', node.get('directory_path', None)) + node_path = node.get("file_path", node.get("directory_path", None)) - if node_path == '/': + if node_path == "/": return expected_parent_dir_path = dirname(node_path) try: - node['parent_directory'] = existing_dirs[expected_parent_dir_path] - except KeyError: # pragma: no cover + node["parent_directory"] = existing_dirs[expected_parent_dir_path] + except KeyError: # pragma: no cover raise Exception( - 'No parent found for path %s, even though existing_dirs had stuff ' - 'in it. This should never happen' % node.get('file_path', node.get('directory_path', None)) + "No parent found for path %s, even though existing_dirs had stuff " + "in it. This should never happen" + % node.get("file_path", node.get("directory_path", None)) ) @staticmethod @@ -1420,7 +1626,9 @@ def calculate_project_directory_byte_sizes_and_file_counts(project_identifier): (Re-)calculate directory byte sizes and file counts in this project. """ try: - project_root_dir = Directory.objects.get(project_identifier=project_identifier, parent_directory_id=None) + project_root_dir = Directory.objects.get( + project_identifier=project_identifier, parent_directory_id=None + ) except Directory.DoesNotExist: # root directory does not exist - all project files have been deleted pass @@ -1429,39 +1637,43 @@ def calculate_project_directory_byte_sizes_and_file_counts(project_identifier): @classmethod def validate_file_characteristics_reference_data(cls, file_characteristics, cache): - reference_data = cls.get_reference_data(cache).get('reference_data', None) + reference_data = cls.get_reference_data(cache).get("reference_data", None) errors = defaultdict(list) - if 'file_format' in file_characteristics: - ff = file_characteristics['file_format'] - fv = file_characteristics.get('format_version', '') + if "file_format" in file_characteristics: + ff = file_characteristics["file_format"] + fv = file_characteristics.get("format_version", "") versions = cls._validate_file_format_and_get_versions_from_reference_data( - reference_data['file_format_version'], ff, errors) + reference_data["file_format_version"], ff, errors + ) # If the given file_format is a valid value, proceed to checking the given format_version value if not errors: # If the given file_format had several output_format_version values in refdata, but the given # format_version is not one of them, it's an error if versions and fv not in versions: - errors['file_characteristics.format_version'].\ - append('Value \'{0}\' for format_version does not match the allowed values for the given ' - 'file_format value \'{1}\' in reference data'.format(fv, ff)) + errors["file_characteristics.format_version"].append( + "Value '{0}' for format_version does not match the allowed values for the given " + "file_format value '{1}' in reference data".format(fv, ff) + ) # If the given file_format did not have any output_format_version values in refdata, but the given # format_version is non-empty, it's an error elif not versions and fv: - errors['file_characteristics.format_version']. \ - append('Any non-empty value for format_version not allowed for the given file_format value ' - '\'{0}\' in reference data'.format(ff)) + errors["file_characteristics.format_version"].append( + "Any non-empty value for format_version not allowed for the given file_format value " + "'{0}' in reference data".format(ff) + ) # If format_version was given but no file_format was given, it's an error - elif 'format_version' in file_characteristics: - errors['file_characteristics.file_format'].append('Value missing') + elif "format_version" in file_characteristics: + errors["file_characteristics.file_format"].append("Value missing") if errors: raise ValidationError(errors) @staticmethod - def _validate_file_format_and_get_versions_from_reference_data(file_format_version_refdata, - input_file_format, errors={}): + def _validate_file_format_and_get_versions_from_reference_data( + file_format_version_refdata, input_file_format, errors={} + ): """ Check if the input_file_format value exists in file_format_version reference data, and if it does, return a list of all possible output_format_version values for the particular input_file_format. @@ -1471,13 +1683,14 @@ def _validate_file_format_and_get_versions_from_reference_data(file_format_versi versions = [] iff_found = False for entry in file_format_version_refdata: - if input_file_format == entry['input_file_format']: + if input_file_format == entry["input_file_format"]: iff_found = True - if entry.get('output_format_version', False): - versions.append(entry['output_format_version']) + if entry.get("output_format_version", False): + versions.append(entry["output_format_version"]) if not iff_found: - errors['file_characteristics.file_format'].append( - 'Value for file_format \'%s\' not found in reference data' % input_file_format) + errors["file_characteristics.file_format"].append( + "Value for file_format '%s' not found in reference data" % input_file_format + ) return versions diff --git a/src/metax_api/services/pagination.py b/src/metax_api/services/pagination.py index 08c11c91..a164a3f3 100755 --- a/src/metax_api/services/pagination.py +++ b/src/metax_api/services/pagination.py @@ -4,13 +4,13 @@ class DirectoryPagination(LimitOffsetPagination): page_size = 10 - page_size_query_param = 'page_size' + page_size_query_param = "page_size" def paginate_directory_data(self, dirs, files, request, view=None): - ''' + """ Takes in directories and files as lists or querysets. Output is list tuple. - ''' + """ self.count = self.get_count([dirs, files]) self.request = request @@ -38,11 +38,11 @@ def paginate_directory_data(self, dirs, files, request, view=None): dirs = [] offset = self.offset - dir_len if files: - files = files[offset:offset + self.limit] + files = files[offset : offset + self.limit] # if directories are not enough for one page limit elif (self.offset + self.limit) > dir_len: - dirs = dirs[self.offset:] + dirs = dirs[self.offset :] if files: files_to_show = self.limit - (dir_len - self.offset) if files_to_show > 0: @@ -50,7 +50,7 @@ def paginate_directory_data(self, dirs, files, request, view=None): # if enough directories for page limit else: - dirs = dirs[self.offset:self.offset + self.limit] + dirs = dirs[self.offset : self.offset + self.limit] if files: files = [] @@ -65,6 +65,8 @@ def get_count(self, contents): for content in contents: if content: - count = count + len(content) if isinstance(content, list) else count + content.count() + count = ( + count + len(content) if isinstance(content, list) else count + content.count() + ) return count diff --git a/src/metax_api/services/rabbitmq_service.py b/src/metax_api/services/rabbitmq_service.py index 3406e0b3..0e541693 100755 --- a/src/metax_api/services/rabbitmq_service.py +++ b/src/metax_api/services/rabbitmq_service.py @@ -7,17 +7,20 @@ import logging import random -from json import dumps as json_dumps +from json import dumps as json_dumps, loads from time import sleep import pika +from django.db import DatabaseError from django.conf import settings from django.core.serializers.json import DjangoJSONEncoder +from metax_api.models import ApiError from metax_api.utils.utils import executing_test_case _logger = logging.getLogger(__name__) + class _RabbitMQService: def __init__(self): if not hasattr(settings, "RABBITMQ"): @@ -27,46 +30,46 @@ def __init__(self): self._settings["USER"], self._settings["PASSWORD"] ) self._hosts = self._settings["HOSTS"] - self._connection = None def _connect(self): - if self._connection and self._connection.is_open: - return + """ + Creates and returns a new BlockingConnection for the caller. Creating a new connection enables multiple + threads (i.e. requests) to access RabbitMQ in parallel using synchronous connection. Even though initializing + a new connection for each request is slow and has some overhead, thread-safety is more important. + """ # Connection retries are needed as long as there is no load balancer in front of rabbitmq-server VMs sleep_time = 1 num_conn_retries = 5 - _logger.info( - f"connecting to RabbitMQ host: {self._hosts} port: {self._settings['PORT']}" - ) + _logger.info(f"connecting to RabbitMQ host: {self._hosts} port: {self._settings['PORT']}") for x in range(0, num_conn_retries): # Choose host randomly so that different hosts are tried out in case of connection problems host = random.choice(self._hosts) try: - kwarg_params = {"port": self._settings["PORT"], "credentials": self._credentials} + kwarg_params = { + "port": self._settings["PORT"], + "credentials": self._credentials, + } if settings.RABBIT_MQ_USE_VHOST: kwarg_params["virtual_host"] = self._settings["VHOST"] conn_params = pika.ConnectionParameters(host, **kwarg_params) - self._connection = pika.BlockingConnection( - conn_params - ) + connection = pika.BlockingConnection(conn_params) except Exception as e: _logger.error( - "Problem connecting to RabbitMQ server (%s), trying to reconnect..." - % str(e) + "Problem connecting to RabbitMQ server (%s), trying to reconnect..." % str(e) ) sleep(sleep_time) else: - self._channel = self._connection.channel() _logger.info("RabbitMQ connected to %s" % host) - break + + return connection else: raise Exception("Unable to connect to RabbitMQ") - def publish(self, body, routing_key='', exchange=None, persistent=True): + def publish(self, body, routing_key="", exchange=None, persistent=True): """ Publish a message to an exchange, which might or might not have queues bound to it. @@ -80,7 +83,8 @@ def publish(self, body, routing_key='', exchange=None, persistent=True): otherwise messages not retrieved by clients before restart will be lost. (still is not 100 % guaranteed to persist!) """ - self._connect() + connection = self._connect() + channel = connection.channel() self._validate_publish_params(routing_key, exchange) additional_args = {} @@ -95,16 +99,42 @@ def publish(self, body, routing_key='', exchange=None, persistent=True): try: for message in messages: if isinstance(message, dict): - message = json_dumps( - message, - cls=DjangoJSONEncoder) - self._channel.basic_publish(body=message, routing_key=routing_key, exchange=exchange, **additional_args) + message = json_dumps(message, cls=DjangoJSONEncoder) + channel.basic_publish( + body=message, + routing_key=routing_key, + exchange=exchange, + **additional_args, + ) except Exception as e: _logger.error(e) _logger.error("Unable to publish message to RabbitMQ") raise finally: - self._connection.close() + connection.close() + + def consume_api_errors(self): + connection = self._connect() + channel = connection.channel() + + try: + for method, _, body in channel.consume("metax-apierrors", inactivity_timeout=1): + if method is None and body is None: + channel.cancel() + break + try: + error = loads(body) + ApiError.objects.create(identifier=error["identifier"], error=error) + except DatabaseError as e: + _logger.error("cannot create API Error. Discarding..") + _logger.debug(f"error: {e}") + finally: + channel.basic_ack(method.delivery_tag) + except Exception as e: + _logger.error(e) + finally: + _logger.debug("All ApiErrors were handled") + connection.close() def init_exchanges(self): """ @@ -112,28 +142,31 @@ def init_exchanges(self): an error will occur if an exchange existed, and it is being re-declared with different settings. In that case the exchange has to be manually removed first, which can result in lost messages. """ - self._connect() + connection = self._connect() + + if connection is None: + return + + channel = connection.channel() try: for exchange in self._settings["EXCHANGES"]: - self._channel.exchange_declare( + channel.exchange_declare( exchange["NAME"], exchange_type=exchange["TYPE"], durable=exchange["DURABLE"], ) for queue in exchange.get("QUEUES", []): # declare queues in settings - self._channel.queue_declare(queue["NAME"], durable=exchange["DURABLE"]) - self._channel.queue_bind( - queue["NAME"], - exchange["NAME"], - queue.get("ROUTING_KEY") + channel.queue_declare(queue["NAME"], durable=exchange["DURABLE"]) + channel.queue_bind( + queue["NAME"], exchange["NAME"], queue.get("ROUTING_KEY") ) except Exception as e: _logger.error(e) _logger.exception("Failed to initialize RabbitMQ exchanges") raise finally: - self._connection.close() + connection.close() def _validate_publish_params(self, routing_key, exchange_name): """ @@ -170,6 +203,8 @@ def publish(self, body, routing_key="", exchange="datasets", persistent=True): def init_exchanges(self, *args, **kwargs): pass + def consume_api_errors(self): + pass if executing_test_case(): RabbitMQService = _RabbitMQServiceDummy() diff --git a/src/metax_api/services/redis_cache_service.py b/src/metax_api/services/redis_cache_service.py index e127c9b6..c51d0988 100755 --- a/src/metax_api/services/redis_cache_service.py +++ b/src/metax_api/services/redis_cache_service.py @@ -88,13 +88,9 @@ def __init__(self, db=0): if not settings.get("SENTINEL", None): raise Exception("Missing configuration from settings for REDIS: SENTINEL") if not settings["SENTINEL"].get("HOSTS", None): - raise Exception( - "Missing configuration from settings for REDIS.SENTINEL: HOSTS" - ) + raise Exception("Missing configuration from settings for REDIS.SENTINEL: HOSTS") if not settings["SENTINEL"].get("SERVICE", None): - raise Exception( - "Missing configuration from settings for REDIS.SENTINEL: SERVICE" - ) + raise Exception("Missing configuration from settings for REDIS.SENTINEL: SERVICE") if not settings.get("TEST_DB", None): raise Exception("Missing configuration from settings for REDIS: TEST_DB") if len(settings["SENTINEL"]["HOSTS"]) < 3: @@ -105,9 +101,7 @@ def __init__(self, db=0): if executing_test_case(): db = settings["TEST_DB"] elif db == settings["TEST_DB"]: - raise Exception( - "Invalid db: db index %d is reserved for test suite execution." % db - ) + raise Exception("Invalid db: db index %d is reserved for test suite execution." % db) self._redis_local = StrictRedis( host="localhost", @@ -245,9 +239,7 @@ def _get_from_master(self, key, **kwargs): res = master.get(key, **kwargs) except (TimeoutError, MasterNotFoundError): if self._DEBUG: - d( - "cache: master timed out also. no read instances available. returning None" - ) + d("cache: master timed out also. no read instances available. returning None") # uh oh, no master available either. either all redis instances have hit the bucket, # or there is a fail-over in process, and a new master will be in line in a moment return None @@ -273,8 +265,7 @@ def _get_slave(self): return self._sentinel.slave_for(self._service_name, socket_timeout=0.1) def _count_nodes(self): - return ( - len(self._sentinel.discover_slaves(self._service_name)) + 1 - ) # +1 is master + return len(self._sentinel.discover_slaves(self._service_name)) + 1 # +1 is master + RedisCacheService = RedisClient diff --git a/src/metax_api/services/reference_data_mixin.py b/src/metax_api/services/reference_data_mixin.py index b62a4e2d..fd80e63e 100755 --- a/src/metax_api/services/reference_data_mixin.py +++ b/src/metax_api/services/reference_data_mixin.py @@ -19,7 +19,7 @@ d = _logger.debug -class ReferenceDataMixin(): +class ReferenceDataMixin: """ Helper methods that other service classes can use when dealing with reference data @@ -30,7 +30,13 @@ class ReferenceDataMixin(): process_cached_reference_data = None @staticmethod - def check_ref_data(ref_data_type, field_to_check, relation_name, errors={}, value_not_found_is_error=True): + def check_ref_data( + ref_data_type, + field_to_check, + relation_name, + errors={}, + value_not_found_is_error=True, + ): """ Check if the given field exists in the reference data. The value of the field can be either the actual uri, or a shorthand code. @@ -46,11 +52,15 @@ def check_ref_data(ref_data_type, field_to_check, relation_name, errors={}, valu relation_name: the full relation path to the field to hand out in case of errors """ try: - return next(entry for entry in ref_data_type if field_to_check in (entry['uri'], entry['code'])) + return next( + entry for entry in ref_data_type if field_to_check in (entry["uri"], entry["code"]) + ) except StopIteration: if value_not_found_is_error: - _logger.error('Identifier \'%s\' not found in reference data' % field_to_check) - errors[relation_name].append('Identifier \'%s\' not found in reference data' % field_to_check) + _logger.error("Identifier '%s' not found in reference data" % field_to_check) + errors[relation_name].append( + "Identifier '%s' not found in reference data" % field_to_check + ) return None @classmethod @@ -68,14 +78,14 @@ def get_reference_data(cls, cache): if cls.process_cached_reference_data is not None: return cls.process_cached_reference_data - ref_data = cache.get('reference_data') + ref_data = cache.get("reference_data") # ref_data += cache.get('reference-data') if ref_data: cls.process_cached_reference_data = ref_data return cls.process_cached_reference_data else: - _logger.info('reference_data missing from cache - attempting to reload') + _logger.info("reference_data missing from cache - attempting to reload") try: state = ReferenceDataLoader.populate_cache_reference_data(cache) @@ -86,33 +96,35 @@ def get_reference_data(cls, cache): # when ref data was just populated, always retrieve from master to ensure # data is found, since there is a delay in data flow to slaves - ref_data = cache.get('reference_data', master=True) + ref_data = cache.get("reference_data", master=True) if ref_data: cls.process_cached_reference_data = ref_data return cls.process_cached_reference_data - elif state == 'reload_started_by_other' and retry < cls.REF_DATA_RELOAD_MAX_RETRIES: + elif state == "reload_started_by_other" and retry < cls.REF_DATA_RELOAD_MAX_RETRIES: sleep(1) - elif state == 'reload_started_by_other' and retry >= cls.REF_DATA_RELOAD_MAX_RETRIES: + elif state == "reload_started_by_other" and retry >= cls.REF_DATA_RELOAD_MAX_RETRIES: cls._raise_reference_data_reload_error( - 'Reload in progress by another request. Retried max times, and gave up' + "Reload in progress by another request. Retried max times, and gave up" ) else: cls._raise_reference_data_reload_error( - 'Current request tried to reload reference data, but apparently failed,' - ' since key reference_data is still missing' + "Current request tried to reload reference data, but apparently failed," + " since key reference_data is still missing" ) - def populate_from_ref_data(ref_entry, obj, uri_field='identifier', label_field=None, add_in_scheme=True): + def populate_from_ref_data( + ref_entry, obj, uri_field="identifier", label_field=None, add_in_scheme=True + ): """ Always populate at least uri field (even if it was alread there, no big deal). Label field population is necessary for some ref data types only. """ - obj[uri_field] = ref_entry['uri'] - if label_field and 'label' in ref_entry: - obj[label_field] = ref_entry['label'] - if add_in_scheme and 'scheme' in ref_entry: - obj['in_scheme'] = ref_entry['scheme'] + obj[uri_field] = ref_entry["uri"] + if label_field and "label" in ref_entry: + obj[label_field] = ref_entry["label"] + if add_in_scheme and "scheme" in ref_entry: + obj["in_scheme"] = ref_entry["scheme"] def _raise_reference_data_reload_error(error): """ @@ -120,16 +132,18 @@ def _raise_reference_data_reload_error(error): with HTTP 503. """ _logger.exception( - 'Failed to reload reference_data from ES - raising 503 temporarily unavailable.' - ' Details:\n%s' % error + "Failed to reload reference_data from ES - raising 503 temporarily unavailable." + " Details:\n%s" % error ) - error_msg = 'Reference data temporarily unavailable. Please try again later' + error_msg = "Reference data temporarily unavailable. Please try again later" if django_settings.DEBUG: - error_msg += ' DEBUG: %s' % str(error) + error_msg += " DEBUG: %s" % str(error) raise Http503(error_msg) @classmethod - def process_org_obj_against_ref_data(cls, orgdata, org_obj, org_obj_relation_name, refdata=None, errors={}): + def process_org_obj_against_ref_data( + cls, orgdata, org_obj, org_obj_relation_name, refdata=None, errors={} + ): """ First check if org object contains is_part_of relation, in which case recursively call this method until there is no is_part_of relation. After this, check whether org object has a value in identifier field. @@ -146,67 +160,116 @@ def process_org_obj_against_ref_data(cls, orgdata, org_obj, org_obj_relation_nam if not orgdata or not org_obj: return - if org_obj.get('is_part_of', False): - nested_obj = org_obj.get('is_part_of') - cls.process_org_obj_against_ref_data(orgdata, nested_obj, - org_obj_relation_name + '.is_part_of', refdata=refdata, errors=errors) - - if org_obj.get('identifier', False): - ref_entry = cls.check_ref_data(orgdata, org_obj['identifier'], - org_obj_relation_name + '.identifier', value_not_found_is_error=False) + if org_obj.get("is_part_of", False): + nested_obj = org_obj.get("is_part_of") + cls.process_org_obj_against_ref_data( + orgdata, + nested_obj, + org_obj_relation_name + ".is_part_of", + refdata=refdata, + errors=errors, + ) + + if org_obj.get("identifier", False): + ref_entry = cls.check_ref_data( + orgdata, + org_obj["identifier"], + org_obj_relation_name + ".identifier", + value_not_found_is_error=False, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, org_obj, 'identifier', 'name', add_in_scheme=False) + cls.populate_from_ref_data( + ref_entry, org_obj, "identifier", "name", add_in_scheme=False + ) - if ref_entry.get('parent_org_code', False) and 'is_part_of' not in org_obj: - parent_ref_entry = cls.check_ref_data(orgdata, ref_entry['parent_org_code'], - org_obj_relation_name + '.is_part_of.identifier', - value_not_found_is_error=False) + if ref_entry.get("parent_org_code", False) and "is_part_of" not in org_obj: + parent_ref_entry = cls.check_ref_data( + orgdata, + ref_entry["parent_org_code"], + org_obj_relation_name + ".is_part_of.identifier", + value_not_found_is_error=False, + ) if parent_ref_entry: parent_org_obj = {} - if 'DataCatalog' not in cls.__name__: + if "DataCatalog" not in cls.__name__: # Datacatalogs does not allow @type field to be populated for organizations - parent_org_obj['@type'] = 'Organization' - - cls.populate_from_ref_data(parent_ref_entry, parent_org_obj, - 'identifier', 'name', add_in_scheme=False) - org_obj['is_part_of'] = parent_org_obj - - if refdata and 'contributor_type' in refdata: - for contributor_type in org_obj.get('contributor_type', []): - ref_entry = cls.check_ref_data(refdata['contributor_type'], contributor_type['identifier'], - org_obj_relation_name + '.contributor_type.identifier', errors) + parent_org_obj["@type"] = "Organization" + + cls.populate_from_ref_data( + parent_ref_entry, + parent_org_obj, + "identifier", + "name", + add_in_scheme=False, + ) + org_obj["is_part_of"] = parent_org_obj + + if refdata and "contributor_type" in refdata: + for contributor_type in org_obj.get("contributor_type", []): + ref_entry = cls.check_ref_data( + refdata["contributor_type"], + contributor_type["identifier"], + org_obj_relation_name + ".contributor_type.identifier", + errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, contributor_type, label_field='pref_label') + cls.populate_from_ref_data( + ref_entry, contributor_type, label_field="pref_label" + ) @classmethod - def process_research_agent_obj_with_type(cls, orgdata, refdata, errors, agent_obj, agent_obj_relation_name): - if agent_obj.get('@type') == 'Person': - member_of = agent_obj.get('member_of', None) + def process_research_agent_obj_with_type( + cls, orgdata, refdata, errors, agent_obj, agent_obj_relation_name + ): + if agent_obj.get("@type") == "Person": + member_of = agent_obj.get("member_of", None) if member_of: - cls.process_org_obj_against_ref_data(orgdata, member_of, agent_obj_relation_name + '.member_of', - refdata=refdata, errors=errors) + cls.process_org_obj_against_ref_data( + orgdata, + member_of, + agent_obj_relation_name + ".member_of", + refdata=refdata, + errors=errors, + ) - for contributor_role in agent_obj.get('contributor_role', []): - ref_entry = cls.check_ref_data(refdata['contributor_role'], contributor_role['identifier'], - agent_obj_relation_name + '.contributor_role.identifier', errors=errors) + for contributor_role in agent_obj.get("contributor_role", []): + ref_entry = cls.check_ref_data( + refdata["contributor_role"], + contributor_role["identifier"], + agent_obj_relation_name + ".contributor_role.identifier", + errors=errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, contributor_role, label_field='pref_label') - - for contributor_type in agent_obj.get('contributor_type', []): - ref_entry = cls.check_ref_data(refdata['contributor_type'], contributor_type['identifier'], - agent_obj_relation_name + '.contributor_type.identifier', errors=errors) + cls.populate_from_ref_data( + ref_entry, contributor_role, label_field="pref_label" + ) + + for contributor_type in agent_obj.get("contributor_type", []): + ref_entry = cls.check_ref_data( + refdata["contributor_type"], + contributor_type["identifier"], + agent_obj_relation_name + ".contributor_type.identifier", + errors=errors, + ) if ref_entry: - cls.populate_from_ref_data(ref_entry, contributor_type, label_field='pref_label') - - elif agent_obj.get('@type') == 'Organization': - cls.process_org_obj_against_ref_data(orgdata, agent_obj, agent_obj_relation_name, refdata=refdata, - errors=errors) + cls.populate_from_ref_data( + ref_entry, contributor_type, label_field="pref_label" + ) + + elif agent_obj.get("@type") == "Organization": + cls.process_org_obj_against_ref_data( + orgdata, + agent_obj, + agent_obj_relation_name, + refdata=refdata, + errors=errors, + ) @classmethod def remove_language_obj_irrelevant_titles(cls, lang_obj, title_label_field): title_obj = lang_obj.get(title_label_field, None) if title_obj: - to_delete = set(title_obj.keys()).difference(['fi', 'sv', 'en', 'und']) + to_delete = set(title_obj.keys()).difference(["fi", "sv", "en", "und"]) for d in to_delete: del title_obj[d] diff --git a/src/metax_api/services/rems_service.py b/src/metax_api/services/rems_service.py index 9cd7f9a4..ffcbeb9f 100755 --- a/src/metax_api/services/rems_service.py +++ b/src/metax_api/services/rems_service.py @@ -12,48 +12,47 @@ _logger = logging.getLogger(__name__) HANDLER_CLOSEABLE_APPLICATIONS = [ - 'application.state/approved', - 'application.state/returned', - 'application.state/submitted' + "application.state/approved", + "application.state/returned", + "application.state/submitted", ] -APPLICANT_CLOSEABLE_APPLICATIONS = [ - 'application.state/draft' -] +APPLICANT_CLOSEABLE_APPLICATIONS = ["application.state/draft"] + class REMSException(Exception): pass -class REMSService(): +class REMSService: def __init__(self): - if not hasattr(django_settings, 'REMS'): - raise Exception('Missing configuration from settings.py: REMS') + if not hasattr(django_settings, "REMS"): + raise Exception("Missing configuration from settings.py: REMS") settings = django_settings.REMS # only reporter_user is privileged to get all applications from REMS - self.api_key = settings['API_KEY'] - self.base_url = settings['BASE_URL'] - self.etsin_url = settings['ETSIN_URL_TEMPLATE'] - self.metax_user = settings['METAX_USER'] - self.reporter_user = settings['REPORTER_USER'] - self.auto_approver = settings['AUTO_APPROVER'] - self.form_id = settings['FORM_ID'] + self.api_key = settings["API_KEY"] + self.base_url = settings["BASE_URL"] + self.etsin_url = settings["ETSIN_URL_TEMPLATE"] + self.metax_user = settings["METAX_USER"] + self.reporter_user = settings["REPORTER_USER"] + self.auto_approver = settings["AUTO_APPROVER"] + self.form_id = settings["FORM_ID"] self.headers = { "x-rems-api-key": self.api_key, "x-rems-user-id": self.metax_user, - "Content-Type": "application/json" + "Content-Type": "application/json", } try: response = requests.get(f"{self.base_url}/health", headers=self.headers) except Exception as e: - raise Exception(f'Cannot connect to rems while checking its health. Error {e}') + raise Exception(f"Cannot connect to rems while checking its health. Error {e}") - if not response.json()['healthy'] is True: - raise REMSException('Rems is not healthy, request is aborted') + if not response.json()["healthy"] is True: + raise REMSException("Rems is not healthy, request is aborted") def create_rems_entity(self, cr, user_info): """ @@ -62,9 +61,9 @@ def create_rems_entity(self, cr, user_info): self.cr = cr # create user. Successful even if userid is already taken - self._post_rems('user', user_info) + self._post_rems("user", user_info) - wf_id = self._create_workflow(user_info['userid']) + wf_id = self._create_workflow(user_info["userid"]) license_id = self._create_license() res_id = self._create_resource(license_id) @@ -78,9 +77,9 @@ def close_rems_entity(self, old_rems_id, reason): self._close_applications(old_rems_id, reason) - self._close_entity('catalogue-item', rems_ci[0]['id']) - self._close_entity('workflow', rems_ci[0]['wfid']) - self._close_entity('resource', rems_ci[0]['resource-id']) + self._close_entity("catalogue-item", rems_ci[0]["id"]) + self._close_entity("workflow", rems_ci[0]["wfid"]) + self._close_entity("resource", rems_ci[0]["resource-id"]) def update_rems_entity(self, cr, old_rems_id, reason): """ @@ -95,22 +94,21 @@ def update_rems_entity(self, cr, old_rems_id, reason): self._close_applications(old_rems_id, reason) - self._close_entity('catalogue-item', rems_ci[0]['id']) - self._close_entity('resource', rems_ci[0]['resource-id']) + self._close_entity("catalogue-item", rems_ci[0]["id"]) + self._close_entity("resource", rems_ci[0]["resource-id"]) license_id = self._create_license() res_id = self._create_resource(license_id) - self._create_catalogue_item(res_id, rems_ci[0]['wfid']) + self._create_catalogue_item(res_id, rems_ci[0]["wfid"]) def _get_catalogue_item(self, rems_id): rems_ci = self._get_rems( - 'catalogue-item', - f'resource={rems_id}&archived=true&disabled=true' + "catalogue-item", f"resource={rems_id}&archived=true&disabled=true" ) - if len(rems_ci) < 1: # pragma: no cover + if len(rems_ci) < 1: # pragma: no cover # this should not happen - raise REMSException(f'Could not find catalogue-item for {rems_id} in REMS.') + raise REMSException(f"Could not find catalogue-item for {rems_id} in REMS.") return rems_ci @@ -121,126 +119,131 @@ def _close_applications(self, rems_id, reason): Furthermore, closed, rejected or revoked applications cannot be closed. """ # REMS only allows reporter_user to get all applications - self.headers['x-rems-user-id'] = self.reporter_user + self.headers["x-rems-user-id"] = self.reporter_user - applications = self._get_rems('application', f'query=resource:\"{rems_id}\"') + applications = self._get_rems("application", f'query=resource:"{rems_id}"') for application in applications: - if application['application/state'] in HANDLER_CLOSEABLE_APPLICATIONS: - closing_user = application['application/workflow']['workflow.dynamic/handlers'][0]['userid'] - elif application['application/state'] in APPLICANT_CLOSEABLE_APPLICATIONS: - closing_user = application['application/applicant']['userid'] + if application["application/state"] in HANDLER_CLOSEABLE_APPLICATIONS: + closing_user = application["application/workflow"]["workflow.dynamic/handlers"][0][ + "userid" + ] + elif application["application/state"] in APPLICANT_CLOSEABLE_APPLICATIONS: + closing_user = application["application/applicant"]["userid"] else: continue - self.headers['x-rems-user-id'] = closing_user + self.headers["x-rems-user-id"] = closing_user - body = {"application-id": application['application/id'], "comment": f"Closed due to dataset {reason}"} + body = { + "application-id": application["application/id"], + "comment": f"Closed due to dataset {reason}", + } - self._post_rems('application', body, 'close') + self._post_rems("application", body, "close") - self.headers['x-rems-user-id'] = self.metax_user + self.headers["x-rems-user-id"] = self.metax_user def _close_entity(self, entity, id): - body_ar = {'id': id, 'archived': True} - body_en = {'id': id, 'enabled': False} + body_ar = {"id": id, "archived": True} + body_en = {"id": id, "enabled": False} - self._put_rems(entity, 'archived', body_ar) - self._put_rems(entity, 'enabled', body_en) + self._put_rems(entity, "archived", body_ar) + self._put_rems(entity, "enabled", body_en) def _create_workflow(self, user_id): body = { "organization": self.cr.metadata_owner_org, - "title": self.cr.research_dataset['preferred_identifier'], - "type": 'workflow/default', - "handlers": [user_id] + "title": self.cr.research_dataset["preferred_identifier"], + "type": "workflow/default", + "handlers": [user_id], } - response = self._post_rems('workflow', body) + response = self._post_rems("workflow", body) - return response['id'] + return response["id"] def _create_license(self): """ Checks if license is already found from REMS before creating new one """ - license = self.cr.research_dataset['access_rights']['license'][0] - license_url = license.get('identifier') or license['license'] + license = self.cr.research_dataset["access_rights"]["license"][0] + license_url = license.get("identifier") or license["license"] # no search parameter provided for license so have to check by hand - rems_licenses = self._get_rems('license', 'disabled=true&archived=true') + rems_licenses = self._get_rems("license", "disabled=true&archived=true") for lic in rems_licenses: - if any( [v['textcontent'] == license_url for v in lic['localizations'].values()] ): - return lic['id'] + if any([v["textcontent"] == license_url for v in lic["localizations"].values()]): + return lic["id"] - body = { - "licensetype": 'link', - "localizations": {} - } + body = {"licensetype": "link", "localizations": {}} - for lang in list(license['title'].keys()): - body['localizations'].update({ - lang: { - "title": license['title'][lang], - "textcontent": license_url - } - }) + for lang in list(license["title"].keys()): + body["localizations"].update( + {lang: {"title": license["title"][lang], "textcontent": license_url}} + ) - response = self._post_rems('license', body) + response = self._post_rems("license", body) - return response['id'] + return response["id"] def _create_resource(self, license_id): body = { "resid": self.cr.rems_identifier, "organization": self.cr.metadata_owner_org, - "licenses": [license_id] + "licenses": [license_id], } - response = self._post_rems('resource', body) + response = self._post_rems("resource", body) - return response['id'] + return response["id"] def _create_catalogue_item(self, res_id, wf_id): - rd_title = self.cr.research_dataset['title'] + rd_title = self.cr.research_dataset["title"] body = { "form": self.form_id, "resid": res_id, "wfid": wf_id, "localizations": {}, - "enabled": True + "enabled": True, } for lang in list(rd_title.keys()): - body['localizations'].update({ - lang: { - "title": rd_title[lang], - "infourl": self.etsin_url % self.cr.identifier + body["localizations"].update( + { + lang: { + "title": rd_title[lang], + "infourl": self.etsin_url % self.cr.identifier, + } } - }) + ) - response = self._post_rems('catalogue-item', body) + response = self._post_rems("catalogue-item", body) - return response['id'] + return response["id"] - def _post_rems(self, entity, body, action='create'): + def _post_rems(self, entity, body, action="create"): """ Send post to REMS. Action is needed as parameter because applications are closed with post. """ try: - response = requests.post(f"{self.base_url}/{entity}s/{action}", json=body, headers=self.headers) + response = requests.post( + f"{self.base_url}/{entity}s/{action}", json=body, headers=self.headers + ) except Exception as e: - raise Exception(f'Connection to REMS failed while creating {entity}. Error: {e}') + raise Exception(f"Connection to REMS failed while creating {entity}. Error: {e}") if response.status_code != 200: - raise REMSException(f'REMS returned bad status while creating {entity}. Error: {response.text}') + raise REMSException( + f"REMS returned bad status while creating {entity}. Error: {response.text}" + ) # operation status is in body resp = response.json() - if not resp['success']: + if not resp["success"]: raise REMSException(f'Could not {action} {entity} to REMS. Error: {resp["errors"]}') return resp @@ -250,31 +253,37 @@ def _put_rems(self, entity, action, body): Edit rems entity. Possible actions: [edit, archived, enabled]. """ try: - response = requests.put(f"{self.base_url}/{entity}s/{action}", json=body, headers=self.headers) + response = requests.put( + f"{self.base_url}/{entity}s/{action}", json=body, headers=self.headers + ) except Exception as e: - raise Exception(f'Connection to REMS failed while updating {entity}. Error: {e}') + raise Exception(f"Connection to REMS failed while updating {entity}. Error: {e}") if response.status_code != 200: - raise REMSException(f'REMS returned bad status while updating {entity}. Error: {response.text}') + raise REMSException( + f"REMS returned bad status while updating {entity}. Error: {response.text}" + ) # operation status is in body resp = response.json() - if not resp['success']: + if not resp["success"]: raise REMSException(f'Could not update {entity} to REMS. Error: {resp["errors"]}') return resp - def _get_rems(self, entity, params=''): + def _get_rems(self, entity, params=""): try: response = requests.get(f"{self.base_url}/{entity}s?{params}", headers=self.headers) except Exception as e: - raise Exception(f'Connection to REMS failed while getting {entity}. Error: {e}') + raise Exception(f"Connection to REMS failed while getting {entity}. Error: {e}") if response.status_code != 200: - raise REMSException(f'REMS returned bad status while getting {entity}. Error: {response.text}') + raise REMSException( + f"REMS returned bad status while getting {entity}. Error: {response.text}" + ) # operation should be successful if status code 200 return response.json() diff --git a/src/metax_api/services/schema_service.py b/src/metax_api/services/schema_service.py index 9efb5f5e..0d494c5d 100755 --- a/src/metax_api/services/schema_service.py +++ b/src/metax_api/services/schema_service.py @@ -18,8 +18,8 @@ _logger = logging.getLogger(__name__) d = logging.getLogger(__name__).debug -class SchemaService(): +class SchemaService: @classmethod def get_all_schemas(cls): schema_dir = cls._get_schema_dir() @@ -27,10 +27,15 @@ def get_all_schemas(cls): # The prefix is stripped from returned strings, # so that results can be used as is for retrieving specific # schema by reusing the get_json_schema function from CommonService. - schema_files = [f[0:f.rfind('_schema.json')] + schema_files = [ + f[0 : f.rfind("_schema.json")] for f in listdir(schema_dir) - if isfile(join(schema_dir, f)) and f.endswith('_schema.json')] - return Response(data={'count': len(schema_files), 'results': schema_files}, status=status.HTTP_200_OK) + if isfile(join(schema_dir, f)) and f.endswith("_schema.json") + ] + return Response( + data={"count": len(schema_files), "results": schema_files}, + status=status.HTTP_200_OK, + ) @classmethod def get_schema_content(cls, name): @@ -39,7 +44,7 @@ def get_schema_content(cls, name): # Trying to make sure that the absolute path of the requested file # starts with the schema folder in order to prevent dangerous path # traversals. - if abspath(file_path).startswith(schema_dir) and isfile('%s_schema.json' % file_path): + if abspath(file_path).startswith(schema_dir) and isfile("%s_schema.json" % file_path): return Response(CS.get_json_schema(schema_dir, name), status=status.HTTP_200_OK) else: raise Http404 @@ -47,4 +52,4 @@ def get_schema_content(cls, name): @staticmethod def _get_schema_dir(): cur_dir = abspath(dirname(__file__)) - return abspath('%s/../api/rest/base/schemas' % cur_dir) + return abspath("%s/../api/rest/base/schemas" % cur_dir) diff --git a/src/metax_api/services/statistic_service.py b/src/metax_api/services/statistic_service.py index 21afeb8a..21d547e7 100755 --- a/src/metax_api/services/statistic_service.py +++ b/src/metax_api/services/statistic_service.py @@ -6,24 +6,25 @@ # :license: MIT import logging - +from collections import OrderedDict from django.conf import settings from django.db import connection +from django.db.models import Count, Sum +from django.db.models.functions import Coalesce from metax_api.exceptions import Http400 -from metax_api.models import CatalogRecord, DataCatalog +from metax_api.models import CatalogRecord, DataCatalog, File _logger = logging.getLogger(__name__) -class StatisticService(): - +class StatisticService: @staticmethod def _get_access_types(): - sql_distinct_access_types = ''' + sql_distinct_access_types = """ select distinct(research_dataset->'access_rights'->'access_type'->>'identifier') from metax_api_catalogrecord - ''' + """ with connection.cursor() as cr: cr.execute(sql_distinct_access_types) @@ -32,26 +33,28 @@ def _get_access_types(): return access_types @classmethod - def count_datasets(cls, - access_type=None, - data_catalog=None, - deprecated=None, - from_date=None, - harvested=None, - latest=True, - legacy=None, - metadata_owner_org=None, - metadata_provider_org=None, - metadata_provider_user=None, - preservation_state=None, - removed=None, - to_date=None): + def count_datasets( + cls, + access_type=None, + data_catalog=None, + deprecated=None, + from_date=None, + harvested=None, + latest=True, + legacy=None, + metadata_owner_org=None, + metadata_provider_org=None, + metadata_provider_user=None, + preservation_state=None, + removed=None, + to_date=None, + ): """ Get simple total record count and total byte size according to given filters. """ - _logger.info('Retrieving total count and byte size...') + _logger.info("Retrieving total count and byte size...") - sql = ''' + sql = """ SELECT count(cr.id) AS count, COALESCE(SUM(COALESCE((research_dataset->>'total_files_byte_size')::bigint, 0)), 0) AS ida_byte_size @@ -59,40 +62,44 @@ def count_datasets(cls, join metax_api_datacatalog as dc on dc.id = cr.data_catalog_id where 1=1 %s - ''' + """ where_args = [] sql_args = [] where_args.append("and state = 'published'") if from_date: - where_args.append('and cr.date_created >= %s::date') + where_args.append("and cr.date_created >= %s::date") sql_args.append(from_date) if to_date: - where_args.append('and cr.date_created <= %s::date') + where_args.append("and cr.date_created <= %s::date") sql_args.append(to_date) if access_type: - where_args.append("and research_dataset->'access_rights'->'access_type'->>'identifier' = %s") + where_args.append( + "and research_dataset->'access_rights'->'access_type'->>'identifier' = %s" + ) sql_args.append(access_type) if data_catalog: try: - DataCatalog.objects.values('id').get(pk=int(data_catalog)) - where_args.append('and dc.id = %s') + DataCatalog.objects.values("id").get(pk=int(data_catalog)) + where_args.append("and dc.id = %s") except DataCatalog.DoesNotExist: - raise Http400({ 'detail': ['Data catalog identifier %s not found' % data_catalog] }) + raise Http400({"detail": ["Data catalog identifier %s not found" % data_catalog]}) except ValueError: try: - DataCatalog.objects.values('id').get(catalog_json__identifier=data_catalog) + DataCatalog.objects.values("id").get(catalog_json__identifier=data_catalog) where_args.append("and dc.catalog_json->>'identifier' = %s") except DataCatalog.DoesNotExist: - raise Http400({ 'detail': ['Data catalog identifier %s not found' % data_catalog] }) + raise Http400( + {"detail": ["Data catalog identifier %s not found" % data_catalog]} + ) sql_args.append(data_catalog) if deprecated is not None: - where_args.append('and deprecated = %s') + where_args.append("and deprecated = %s") sql_args.append(deprecated) if harvested: @@ -100,45 +107,52 @@ def count_datasets(cls, sql_args.append(harvested) if latest: - where_args.append('and next_dataset_version_id is null') + where_args.append("and next_dataset_version_id is null") if metadata_owner_org: - where_args.append('and metadata_owner_org = %s') + where_args.append("and metadata_owner_org = %s") sql_args.append(metadata_owner_org) if metadata_provider_org: - where_args.append('and metadata_provider_org = %s') + where_args.append("and metadata_provider_org = %s") sql_args.append(metadata_provider_org) if metadata_provider_user: - where_args.append('and metadata_provider_user = %s') + where_args.append("and metadata_provider_user = %s") sql_args.append(metadata_provider_user) if preservation_state: - where_args.append('and preservation_state = %s') + where_args.append("and preservation_state = %s") sql_args.append(preservation_state) if removed is not None: - where_args.append('and cr.removed = %s') + where_args.append("and cr.removed = %s") sql_args.append(removed) if legacy is not None: - where_args.append(''.join(["and dc.catalog_json->>'identifier'", " = " if legacy else " != ", "any(%s)"])) + where_args.append( + "".join( + [ + "and dc.catalog_json->>'identifier'", + " = " if legacy else " != ", + "any(%s)", + ] + ) + ) sql_args.append(settings.LEGACY_CATALOGS) - sql = sql % '\n'.join(where_args) + sql = sql % "\n".join(where_args) with connection.cursor() as cr: cr.execute(sql, sql_args) try: results = [ - dict(zip([col[0] for col in cr.description], row)) - for row in cr.fetchall() + dict(zip([col[0] for col in cr.description], row)) for row in cr.fetchall() ][0] except IndexError: results = {} - _logger.info('Done retrieving total count and byte size') + _logger.info("Done retrieving total count and byte size") return results @@ -147,9 +161,9 @@ def total_datasets(cls, from_date, to_date, latest=True, legacy=None, removed=No """ Retrieve dataset count and byte size per month and monthly cumulative from all datasets. """ - _logger.info('Retrieving total count and byte sizes for all datasets...') + _logger.info("Retrieving total count and byte sizes for all datasets...") - sql_all_datasets = ''' + sql_all_datasets = """ WITH cte AS ( SELECT date_trunc('month', cr.date_created) AS mon, @@ -182,36 +196,39 @@ def total_datasets(cls, from_date, to_date, latest=True, legacy=None, removed=No ) cr USING (mon) GROUP BY mon, c.mon_ida_byte_size, count ORDER BY mon; - ''' + """ filter_sql = [] filter_args = [] if latest: - filter_sql.append('and next_dataset_version_id is null') + filter_sql.append("and next_dataset_version_id is null") if removed is not None: - filter_sql.append('and cr.removed = %s') + filter_sql.append("and cr.removed = %s") filter_args.append(removed) if legacy is not None: - filter_sql.append(''.join(["and dc.catalog_json->>'identifier'", " = " if legacy else " != ", "any(%s)"])) + filter_sql.append( + "".join( + [ + "and dc.catalog_json->>'identifier'", + " = " if legacy else " != ", + "any(%s)", + ] + ) + ) filter_args.append(settings.LEGACY_CATALOGS) - sql_all_datasets = sql_all_datasets.replace( - 'OPTIONAL_WHERE_FILTERS', - '\n'.join(filter_sql)) + sql_all_datasets = sql_all_datasets.replace("OPTIONAL_WHERE_FILTERS", "\n".join(filter_sql)) - sql_args = filter_args + [from_date + '-01', to_date + '-01'] + filter_args + sql_args = filter_args + [from_date + "-01", to_date + "-01"] + filter_args with connection.cursor() as cr: cr.execute(sql_all_datasets, sql_args) - results = [ - dict(zip([col[0] for col in cr.description], row)) - for row in cr.fetchall() - ] + results = [dict(zip([col[0] for col in cr.description], row)) for row in cr.fetchall()] - _logger.info('Done retrieving total count and byte sizes') + _logger.info("Done retrieving total count and byte sizes") return results @@ -223,37 +240,39 @@ def total_data_catalog_datasets(cls, from_date, to_date, data_catalog=None): """ if data_catalog: try: - dc_params = { 'pk': int(data_catalog) } + dc_params = {"pk": int(data_catalog)} except ValueError: - dc_params = { 'catalog_json__identifier': data_catalog } + dc_params = {"catalog_json__identifier": data_catalog} try: dc = DataCatalog.objects.filter(**dc_params).values() except: - raise Http400({ 'detail': ['Data catalog identifier %s not found' % data_catalog] }) + raise Http400({"detail": ["Data catalog identifier %s not found" % data_catalog]}) _logger.info( - 'Retrieving total count and byte sizes for datasets in catalog: %s' % dc['catalog_json']['identifier'] + "Retrieving total count and byte sizes for datasets in catalog: %s" + % dc["catalog_json"]["identifier"] ) catalogs = [dc] else: - _logger.info('Retrieving total count and byte sizes for datasets in all catalogs') + _logger.info("Retrieving total count and byte sizes for datasets in all catalogs") catalogs = DataCatalog.objects.all().values() access_types = cls._get_access_types() results = {} for dc in catalogs: - results[dc['catalog_json']['identifier']] \ - = cls._total_data_catalog_datasets(from_date, to_date, access_types, dc['id']) + results[dc["catalog_json"]["identifier"]] = cls._total_data_catalog_datasets( + from_date, to_date, access_types, dc["id"] + ) - _logger.info('Done retrieving total count and byte sizes') + _logger.info("Done retrieving total count and byte sizes") return results @classmethod def _total_data_catalog_datasets(cls, from_date, to_date, access_types, dc_id): - sql = ''' + sql = """ WITH cte AS ( SELECT date_trunc('month', cr.date_created) AS mon, @@ -291,7 +310,7 @@ def _total_data_catalog_datasets(cls, from_date, to_date, access_types, dc_id): ) cr USING (mon) GROUP BY mon, c.mon_ida_byte_size, count, access_type ORDER BY mon; - ''' + """ # group results by access_type grouped = {} @@ -300,10 +319,9 @@ def _total_data_catalog_datasets(cls, from_date, to_date, access_types, dc_id): for access_type in access_types: cr.execute(sql, [dc_id, access_type, from_date, to_date, dc_id, access_type]) results = [ - dict(zip([col[0] for col in cr.description], row)) - for row in cr.fetchall() + dict(zip([col[0] for col in cr.description], row)) for row in cr.fetchall() ] - grouped[access_type.split('/')[-1]] = results + grouped[access_type.split("/")[-1]] = results total = [] @@ -315,14 +333,14 @@ def _total_data_catalog_datasets(cls, from_date, to_date, access_types, dc_id): except IndexError: total.append(stats) else: - last['count'] += stats['count'] - last['count_cumulative'] += stats['count_cumulative'] - last['ida_byte_size'] += stats['ida_byte_size'] - last['ida_byte_size_cumulative'] += stats['ida_byte_size_cumulative'] + last["count"] += stats["count"] + last["count_cumulative"] += stats["count_cumulative"] + last["ida_byte_size"] += stats["ida_byte_size"] + last["ida_byte_size_cumulative"] += stats["ida_byte_size_cumulative"] - grouped['total'] = total + grouped["total"] = total - _logger.info('Done retrieving total count and byte sizes') + _logger.info("Done retrieving total count and byte sizes") return grouped @@ -333,14 +351,18 @@ def total_organization_datasets(cls, from_date, to_date, metadata_owner_org=None or a given single organization, grouped by catalog. """ if metadata_owner_org: - _logger.info('Retrieving total count and byte sizes for datasets for organization: %s' % metadata_owner_org) + _logger.info( + "Retrieving total count and byte sizes for datasets for organization: %s" + % metadata_owner_org + ) metadata_owner_orgs = [metadata_owner_org] else: - _logger.info('Retrieving total count and byte sizes for datasets for all organizations') - metadata_owner_orgs = CatalogRecord.objects \ - .values_list('metadata_owner_org', flat=True) \ - .order_by('metadata_owner_org') \ - .distinct('metadata_owner_org') + _logger.info("Retrieving total count and byte sizes for datasets for all organizations") + metadata_owner_orgs = ( + CatalogRecord.objects.values_list("metadata_owner_org", flat=True) + .order_by("metadata_owner_org") + .distinct("metadata_owner_org") + ) results = {} for org in metadata_owner_orgs: @@ -350,7 +372,7 @@ def total_organization_datasets(cls, from_date, to_date, metadata_owner_org=None @classmethod def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): - sql = ''' + sql = """ WITH cte AS ( SELECT date_trunc('month', cr.date_created) AS mon, @@ -385,21 +407,32 @@ def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): ) cr USING (mon) GROUP BY mon, c.mon_ida_byte_size, count ORDER BY mon; - ''' + """ - catalogs = DataCatalog.objects.filter(catalog_json__research_dataset_schema__in=['ida', 'att']).values() + catalogs = DataCatalog.objects.filter( + catalog_json__research_dataset_schema__in=["ida", "att"] + ).values() # group results by catalogs grouped = {} with connection.cursor() as cr: for dc in catalogs: - cr.execute(sql, [dc['id'], metadata_owner_org, from_date, to_date, dc['id'], metadata_owner_org]) + cr.execute( + sql, + [ + dc["id"], + metadata_owner_org, + from_date, + to_date, + dc["id"], + metadata_owner_org, + ], + ) results = [ - dict(zip([col[0] for col in cr.description], row)) - for row in cr.fetchall() + dict(zip([col[0] for col in cr.description], row)) for row in cr.fetchall() ] - grouped[dc['catalog_json']['identifier']] = results + grouped[dc["catalog_json"]["identifier"]] = results total = [] @@ -411,14 +444,14 @@ def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): except IndexError: total.append(stats) else: - last['count'] += stats['count'] - last['count_cumulative'] += stats['count_cumulative'] - last['ida_byte_size'] += stats['ida_byte_size'] - last['ida_byte_size_cumulative'] += stats['ida_byte_size_cumulative'] + last["count"] += stats["count"] + last["count_cumulative"] += stats["count_cumulative"] + last["ida_byte_size"] += stats["ida_byte_size"] + last["ida_byte_size_cumulative"] += stats["ida_byte_size_cumulative"] - grouped['total'] = total + grouped["total"] = total - _logger.info('Done retrieving total count and byte sizes') + _logger.info("Done retrieving total count and byte sizes") return grouped @@ -428,9 +461,9 @@ def total_harvested_datasets(cls, from_date, to_date): For harvested datasets, retrieve dataset count per month and monthly cumulative, and grouped by access_type. """ - _logger.info('Retrieving total counts for harvested datasets') + _logger.info("Retrieving total counts for harvested datasets") - sql = ''' + sql = """ SELECT to_char(mon, 'YYYY-MM') as month, COALESCE(count, 0) as count, @@ -451,7 +484,7 @@ def total_harvested_datasets(cls, from_date, to_date): ) cr USING (mon) GROUP BY mon, count, access_type ORDER BY mon; - ''' + """ access_types = cls._get_access_types() @@ -462,10 +495,9 @@ def total_harvested_datasets(cls, from_date, to_date): for access_type in access_types: cr.execute(sql, [from_date, to_date, access_type]) results = [ - dict(zip([col[0] for col in cr.description], row)) - for row in cr.fetchall() + dict(zip([col[0] for col in cr.description], row)) for row in cr.fetchall() ] - grouped[access_type.split('/')[-1]] = results + grouped[access_type.split("/")[-1]] = results total = [] @@ -477,12 +509,12 @@ def total_harvested_datasets(cls, from_date, to_date): except IndexError: total.append(stats) else: - last['count'] += stats['count'] - last['count_cumulative'] += stats['count_cumulative'] + last["count"] += stats["count"] + last["count_cumulative"] += stats["count_cumulative"] - grouped['total'] = total + grouped["total"] = total - _logger.info('Done retrieving total counts') + _logger.info("Done retrieving total counts") return grouped @@ -491,9 +523,9 @@ def deprecated_datasets_cumulative(cls, from_date=None, to_date=None): """ Retrieve dataset count per month and monthly cumulative for deprecated datasets. """ - _logger.info('Retrieving total counts for deprecated datasets') + _logger.info("Retrieving total counts for deprecated datasets") - sql = ''' + sql = """ SELECT to_char(mon, 'YYYY-MM') as month, COALESCE(count, 0) as count, @@ -509,16 +541,13 @@ def deprecated_datasets_cumulative(cls, from_date=None, to_date=None): ) cr USING (mon) GROUP BY mon, count ORDER BY mon; - ''' + """ with connection.cursor() as cr: cr.execute(sql, [from_date, to_date]) - results = [ - dict(zip([col[0] for col in cr.description], row)) - for row in cr.fetchall() - ] + results = [dict(zip([col[0] for col in cr.description], row)) for row in cr.fetchall()] - _logger.info('Done retrieving total deprecated counts') + _logger.info("Done retrieving total deprecated counts") return results @@ -528,9 +557,9 @@ def total_end_user_datasets(cls, from_date, to_date): Retrieve dataset count per month and monthly cumulative for datasets which have been created by end users using End User API. """ - _logger.info('Retrieving total counts for datasets created using End User API') + _logger.info("Retrieving total counts for datasets created using End User API") - sql = ''' + sql = """ SELECT to_char(mon, 'YYYY-MM') as month, COALESCE(count, 0) as count, @@ -547,24 +576,21 @@ def total_end_user_datasets(cls, from_date, to_date): ) cr USING (mon) GROUP BY mon, count ORDER BY mon; - ''' + """ with connection.cursor() as cr: cr.execute(sql, [from_date, to_date]) - results = [ - dict(zip([col[0] for col in cr.description], row)) - for row in cr.fetchall() - ] + results = [dict(zip([col[0] for col in cr.description], row)) for row in cr.fetchall()] - _logger.info('Done retrieving total counts') + _logger.info("Done retrieving total counts") return results @classmethod def unused_files(cls): - _logger.info('Retrieving total counts of files which are not part of any datasets...') + _logger.info("Retrieving total counts of files which are not part of any datasets...") - sql_get_unused_files_by_project = ''' + sql_get_unused_files_by_project = """ select count(f.id) as count, project_identifier from metax_api_file as f where not exists ( @@ -573,14 +599,30 @@ def unused_files(cls): where file_id = f.id ) group by project_identifier; - ''' + """ with connection.cursor() as cr: cr.execute(sql_get_unused_files_by_project) file_stats = [ - dict(zip([col[0] for col in cr.description], row)) - for row in cr.fetchall() + dict(zip([col[0] for col in cr.description], row)) for row in cr.fetchall() ] - _logger.info('Done retrieving total counts') + _logger.info("Done retrieving total counts") + + return file_stats - return file_stats \ No newline at end of file + @classmethod + def count_files(cls, projects, removed=None): + kwargs = OrderedDict() + file_query = File.objects_unfiltered.all() + + kwargs['project_identifier__in'] = projects + kwargs['record__state'] = "published" + if removed is not None: + kwargs['removed'] = False if removed == 'false' else True + # "record" is defined for CatalogRecord to enable lookups from Files to CatalogRecord + file_query = file_query.filter(**kwargs) \ + .values("id", "byte_size") \ + .distinct() + + # Coalesce is required to provides default value + return file_query.aggregate(count=Count("id"), byte_size=Coalesce(Sum("byte_size"), 0)) diff --git a/src/metax_api/settings/__init__.py b/src/metax_api/settings/__init__.py index 38530c6f..b091c712 100755 --- a/src/metax_api/settings/__init__.py +++ b/src/metax_api/settings/__init__.py @@ -14,9 +14,7 @@ from metax_api.settings.components import BASE_DIR # src # Managing environment via DJANGO_ENV variable: -REFDATA_INDEXER_PATH = join( - BASE_DIR, "metax_api", "tasks", "refdata", "refdata_indexer" -) +REFDATA_INDEXER_PATH = join(BASE_DIR, "metax_api", "tasks", "refdata", "refdata_indexer") env = environ.Env( # set casting, default value LOGGING_LEVEL=(str, "INFO"), @@ -25,8 +23,9 @@ ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART=(bool, True), API_USERS_PATH=(str, "/etc/fairdata-metax/api_users"), DEBUG=(bool, False), + DEBUG_TOOLBAR_ENABLED=(bool, True), DJANGO_ENV=(str, "local"), - ELASTIC_SEARCH_HOSTS=(list, ['localhost']), + ELASTIC_SEARCH_HOSTS=(list, ["localhost"]), ELASTIC_SEARCH_PORT=(int, 9200), ELASTIC_SEARCH_USE_SSL=(bool, False), ENABLE_V1_ENDPOINTS=(bool, True), @@ -34,14 +33,20 @@ ENABLE_DJANGO_WATCHMAN=(bool, False), ERROR_FILES_PATH=(str, join("/var", "log", "metax-api", "errors")), ES_CONFIG_DIR=(str, join(REFDATA_INDEXER_PATH, "resources", "es-config/")), - LOCAL_REF_DATA_FOLDER=(str, join(REFDATA_INDEXER_PATH, "resources", "local-refdata/")), + LOCAL_REF_DATA_FOLDER=( + str, + join(REFDATA_INDEXER_PATH, "resources", "local-refdata/"), + ), LOGGING_PATH=(str, join("/var", "log", "metax-api")), METAX_DATABASE_HOST=(str, "localhost"), METAX_DATABASE_PORT=(str, 5432), - ORG_FILE_PATH=(str, join(REFDATA_INDEXER_PATH, "resources", "organizations", "organizations.csv"),), + ORG_FILE_PATH=( + str, + join(REFDATA_INDEXER_PATH, "resources", "organizations", "organizations.csv"), + ), OAI_BASE_URL=(str, "https://metax.fd-dev.csc.fi/oai/"), OAI_BATCH_SIZE=(int, 25), - OAI_REPOSITORY_NAME=(str, 'Metax'), + OAI_REPOSITORY_NAME=(str, "Metax"), RABBIT_MQ_HOSTS=(list, ["localhost"]), RABBIT_MQ_PORT=(int, 5672), RABBIT_MQ_PASSWORD=(str, "guest"), @@ -55,6 +60,8 @@ SERVER_DOMAIN_NAME=(str, "metax.fd-dev.csc.fi"), VALIDATE_TOKEN_URL=(str, "https://127.0.0.1/secure/validate_token"), WKT_FILENAME=(str, join(REFDATA_INDEXER_PATH, "resources", "uri_to_wkt.json")), + SWAGGER_YAML_PATH=(str, join(BASE_DIR, "metax_api", "swagger")), + SWAGGER_HTML_PATH=(str, join(BASE_DIR, "metax_api", "templates", "swagger")), ) # reading .env file environ.Env.read_env() diff --git a/src/metax_api/settings/components/access_control.py b/src/metax_api/settings/components/access_control.py index 852d1b95..387c0c90 100755 --- a/src/metax_api/settings/components/access_control.py +++ b/src/metax_api/settings/components/access_control.py @@ -4,23 +4,22 @@ from box import Box -api_permissions = Box({ - "rest": { - "apierrors": {}, - "datacatalogs": {}, - "datasets": {}, - "directories": {}, - "files": {}, - "filestorages": {}, - "schemas": {} +api_permissions = Box( + { + "rest": { + "apierrors": {}, + "datacatalogs": {}, + "datasets": {}, + "directories": {}, + "files": {}, + "filestorages": {}, + "schemas": {}, + }, + "rpc": {"datasets": {}, "elasticsearchs": {}, "files": {}, "statistics": {}}, }, - "rpc": { - "datasets": {}, - "elasticsearchs": {}, - "files": {}, - "statistics": {} - } -}, default_box_attr={}, default_box=True) + default_box_attr={}, + default_box=True, +) class Role(Enum): @@ -69,16 +68,57 @@ def __lt__(self, other): api_permissions.rest.datacatalogs["update"] = [Role.METAX, Role.ETSIN] api_permissions.rest.datacatalogs.delete = [Role.METAX, Role.ETSIN] -api_permissions.rest.datasets.create = [Role.METAX, Role.END_USERS, Role.TPAS, Role.QVAIN, Role.ETSIN] +api_permissions.rest.datasets.create = [ + Role.METAX, + Role.END_USERS, + Role.TPAS, + Role.QVAIN, + Role.ETSIN, +] api_permissions.rest.datasets.read = [Role.ALL] -api_permissions.rest.datasets["update"] = [Role.METAX, Role.END_USERS, Role.TPAS, Role.QVAIN, Role.ETSIN] -api_permissions.rest.datasets.delete = [Role.METAX, Role.END_USERS, Role.TPAS, Role.QVAIN, Role.ETSIN] - -api_permissions.rest.directories.read = [Role.METAX, Role.QVAIN, Role.ETSIN, Role.TPAS, Role.FDS, Role.END_USERS] +api_permissions.rest.datasets["update"] = [ + Role.METAX, + Role.END_USERS, + Role.TPAS, + Role.QVAIN, + Role.ETSIN, +] +api_permissions.rest.datasets.delete = [ + Role.METAX, + Role.END_USERS, + Role.TPAS, + Role.QVAIN, + Role.ETSIN, +] + +api_permissions.rest.directories.read = [ + Role.METAX, + Role.QVAIN, + Role.ETSIN, + Role.TPAS, + Role.FDS, + Role.END_USERS, +] api_permissions.rest.files.create = [Role.METAX, Role.IDA, Role.TPAS] -api_permissions.rest.files.read = [Role.METAX, Role.IDA, Role.FDS, Role.TPAS, Role.END_USERS] -api_permissions.rest.files["update"] = [Role.METAX, Role.IDA, Role.TPAS, Role.FDS, Role.END_USERS] +api_permissions.rest.files.read = [ + Role.METAX, + Role.IDA, + Role.FDS, + Role.TPAS, + Role.END_USERS, + Role.QVAIN, + Role.QVAIN_LIGHT, +] +api_permissions.rest.files["update"] = [ + Role.METAX, + Role.IDA, + Role.TPAS, + Role.FDS, + Role.END_USERS, + Role.QVAIN, + Role.QVAIN_LIGHT, +] api_permissions.rest.files.delete = [Role.METAX, Role.IDA, Role.TPAS] api_permissions.rest.filestorages.create = [Role.METAX] @@ -106,12 +146,14 @@ def __lt__(self, other): api_permissions.rpc.statistics.all_datasets_cumulative.use = [Role.ALL] api_permissions.rpc.statistics.catalog_datasets_cumulative.use = [Role.ALL] api_permissions.rpc.statistics.count_datasets.use = [Role.ALL] +api_permissions.rpc.statistics.count_files.use = [Role.ALL] api_permissions.rpc.statistics.deprecated_datasets_cumulative.use = [Role.ALL] api_permissions.rpc.statistics.end_user_datasets_cumulative.use = [Role.ALL] api_permissions.rpc.statistics.harvested_datasets_cumulative.use = [Role.ALL] api_permissions.rpc.statistics.organization_datasets_cumulative.use = [Role.ALL] api_permissions.rpc.statistics.unused_files.use = [Role.ALL] + def prepare_perm_values(d): new_d = d if hasattr(d, "items"): @@ -130,4 +172,4 @@ def prepare_perm_values(d): return new_d -API_ACCESS = prepare_perm_values(api_permissions.to_dict()) \ No newline at end of file +API_ACCESS = prepare_perm_values(api_permissions.to_dict()) diff --git a/src/metax_api/settings/components/common.py b/src/metax_api/settings/components/common.py index 59bd1a31..24529eb2 100755 --- a/src/metax_api/settings/components/common.py +++ b/src/metax_api/settings/components/common.py @@ -17,7 +17,7 @@ ATT_DATA_CATALOG_IDENTIFIER, LEGACY_DATA_CATALOG_IDENTIFIER, DFT_DATA_CATALOG_IDENTIFIER, - PAS_DATA_CATALOG_IDENTIFIER + PAS_DATA_CATALOG_IDENTIFIER, ] # catalogs where uniqueness of dataset pids is not enforced. @@ -188,3 +188,6 @@ {"password": "test-fds", "username": "fds"}, {"password": "test-download", "username": "download"}, ] + +SWAGGER_YAML_PATH = env('SWAGGER_YAML_PATH') +SWAGGER_HTML_PATH = env('SWAGGER_HTML_PATH') diff --git a/src/metax_api/settings/components/elasticsearch.py b/src/metax_api/settings/components/elasticsearch.py index ead9c790..8227bc75 100755 --- a/src/metax_api/settings/components/elasticsearch.py +++ b/src/metax_api/settings/components/elasticsearch.py @@ -3,7 +3,7 @@ ELASTICSEARCH = { "HOSTS": env("ELASTIC_SEARCH_HOSTS"), "PORT": env("ELASTIC_SEARCH_PORT"), - "USE_SSL": env("ELASTIC_SEARCH_USE_SSL") + "USE_SSL": env("ELASTIC_SEARCH_USE_SSL"), } ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART = env("ALWAYS_RELOAD_REFERENCE_DATA_ON_RESTART") diff --git a/src/metax_api/settings/components/externals.py b/src/metax_api/settings/components/externals.py index 93590ac4..b27fb9db 100755 --- a/src/metax_api/settings/components/externals.py +++ b/src/metax_api/settings/components/externals.py @@ -1,5 +1,8 @@ from metax_api.settings import env -from metax_api.settings.components.common import ATT_DATA_CATALOG_IDENTIFIER, IDA_DATA_CATALOG_IDENTIFIER +from metax_api.settings.components.common import ( + ATT_DATA_CATALOG_IDENTIFIER, + IDA_DATA_CATALOG_IDENTIFIER, +) OAI = { "BASE_URL": env("OAI_BASE_URL"), diff --git a/src/metax_api/settings/components/rabbitmq.py b/src/metax_api/settings/components/rabbitmq.py index 8bf1809b..d6f78f7a 100755 --- a/src/metax_api/settings/components/rabbitmq.py +++ b/src/metax_api/settings/components/rabbitmq.py @@ -9,17 +9,30 @@ { "NAME": "datasets", "TYPE": "direct", + "EXC_TYPE": "dataset", # make rabbitmq remember queues after restarts "DURABLE": True, }, { "NAME": "TTV-datasets", "TYPE": "fanout", + "EXC_TYPE": "dataset", "DURABLE": True, "QUEUES": [ { "NAME": "ttv-operations", - #"ROUTING_KEY": "some_key" + # "ROUTING_KEY": "some_key" + } + ], + }, + { + "NAME": "apierrors", + "TYPE": "fanout", + "EXC_TYPE": "other", + "DURABLE": True, + "QUEUES": [ + { + "NAME": "metax-apierrors" } ] } diff --git a/src/metax_api/settings/environments/local.py b/src/metax_api/settings/environments/local.py index 02899153..bed7702b 100755 --- a/src/metax_api/settings/environments/local.py +++ b/src/metax_api/settings/environments/local.py @@ -1,17 +1,17 @@ +from metax_api.settings import env from metax_api.settings.components.access_control import Role, api_permissions, prepare_perm_values from metax_api.settings.components.common import ALLOWED_HOSTS, DEBUG, INSTALLED_APPS, MIDDLEWARE + ALLOWED_HOSTS += ["*"] -if 'debug_toolbar' not in INSTALLED_APPS: - INSTALLED_APPS += ['debug_toolbar'] -if 'debug_toolbar.middleware.DebugToolbarMiddleware' not in MIDDLEWARE: - MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE +if "debug_toolbar" not in INSTALLED_APPS and env("DEBUG_TOOLBAR_ENABLED"): + INSTALLED_APPS += ["debug_toolbar"] +if "debug_toolbar.middleware.DebugToolbarMiddleware" not in MIDDLEWARE and env("DEBUG_TOOLBAR_ENABLED"): + MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE + +INTERNAL_IPS = ["127.0.0.1", "0.0.0.0"] -INTERNAL_IPS = [ - '127.0.0.1', - '0.0.0.0' -] def show_toolbar(request): if DEBUG: @@ -19,6 +19,7 @@ def show_toolbar(request): else: return False + DEBUG_TOOLBAR_CONFIG = { "SHOW_TOOLBAR_CALLBACK": show_toolbar, } diff --git a/src/metax_api/settings/environments/production.py b/src/metax_api/settings/environments/production.py index 9506ea86..cf5223cb 100644 --- a/src/metax_api/settings/environments/production.py +++ b/src/metax_api/settings/environments/production.py @@ -3,4 +3,4 @@ api_permissions.rpc.files.flush_project.use.clear() -API_ACCESS = prepare_perm_values(api_permissions.to_dict()) \ No newline at end of file +API_ACCESS = prepare_perm_values(api_permissions.to_dict()) diff --git a/src/metax_api/settings/environments/stable.py b/src/metax_api/settings/environments/stable.py index fa7e2520..2f59d398 100644 --- a/src/metax_api/settings/environments/stable.py +++ b/src/metax_api/settings/environments/stable.py @@ -1,4 +1,3 @@ - from metax_api.settings.components.access_control import Role, api_permissions, prepare_perm_values from metax_api.settings.environments.staging import API_USERS # noqa: F401 @@ -8,11 +7,23 @@ api_permissions.rest.directories.read += [Role.IDA, Role.QVAIN_LIGHT] -api_permissions.rest.files.read += [Role.QVAIN, Role.QVAIN_LIGHT] -api_permissions.rest.files["update"] += [Role.QVAIN, Role.QVAIN_LIGHT] - -api_permissions.rpc.datasets.change_cumulative_state.use = [Role.METAX, Role.QVAIN, Role.QVAIN_LIGHT, Role.END_USERS] -api_permissions.rpc.datasets.fix_deprecated.use = [Role.METAX, Role.QVAIN, Role.QVAIN_LIGHT, Role.END_USERS] -api_permissions.rpc.dataset.refresh_directory_content.use = [Role.METAX, Role.QVAIN, Role.QVAIN_LIGHT, Role.END_USERS] +api_permissions.rpc.datasets.change_cumulative_state.use = [ + Role.METAX, + Role.QVAIN, + Role.QVAIN_LIGHT, + Role.END_USERS, +] +api_permissions.rpc.datasets.fix_deprecated.use = [ + Role.METAX, + Role.QVAIN, + Role.QVAIN_LIGHT, + Role.END_USERS, +] +api_permissions.rpc.dataset.refresh_directory_content.use = [ + Role.METAX, + Role.QVAIN, + Role.QVAIN_LIGHT, + Role.END_USERS, +] -API_ACCESS = prepare_perm_values(api_permissions) \ No newline at end of file +API_ACCESS = prepare_perm_values(api_permissions) diff --git a/src/metax_api/settings/environments/unittests.py b/src/metax_api/settings/environments/unittests.py index 3a279479..631d9182 100755 --- a/src/metax_api/settings/environments/unittests.py +++ b/src/metax_api/settings/environments/unittests.py @@ -27,9 +27,21 @@ api_permissions.rest.datacatalogs["update"] += [Role.TEST_USER] api_permissions.rest.datacatalogs.delete += [Role.TEST_USER] -api_permissions.rest.datasets.create += [Role.API_AUTH_USER, Role.EXTERNAL, Role.TEST_USER] -api_permissions.rest.datasets["update"] += [Role.API_AUTH_USER, Role.EXTERNAL, Role.TEST_USER] -api_permissions.rest.datasets.delete += [Role.API_AUTH_USER, Role.EXTERNAL, Role.TEST_USER] +api_permissions.rest.datasets.create += [ + Role.API_AUTH_USER, + Role.EXTERNAL, + Role.TEST_USER, +] +api_permissions.rest.datasets["update"] += [ + Role.API_AUTH_USER, + Role.EXTERNAL, + Role.TEST_USER, +] +api_permissions.rest.datasets.delete += [ + Role.API_AUTH_USER, + Role.EXTERNAL, + Role.TEST_USER, +] api_permissions.rest.directories.read += [Role.TEST_USER] diff --git a/src/metax_api/signals/__init__.py b/src/metax_api/signals/__init__.py new file mode 100644 index 00000000..3c389606 --- /dev/null +++ b/src/metax_api/signals/__init__.py @@ -0,0 +1,2 @@ +from .post_delete import * +from .request_finished import * \ No newline at end of file diff --git a/src/metax_api/signals/post_delete.py b/src/metax_api/signals/post_delete.py new file mode 100644 index 00000000..1ff39f75 --- /dev/null +++ b/src/metax_api/signals/post_delete.py @@ -0,0 +1,27 @@ +import json +import logging + +from django.db.models.signals import post_delete +from django.dispatch import receiver +from django.core.serializers.json import DjangoJSONEncoder +from django.forms.models import model_to_dict +from ..models import DeletedObject + +_logger = logging.getLogger(__name__) + +@receiver(post_delete) +def deleted_object_receiver(instance, *args, **kwargs): + try: + model_type = instance._meta.model.__name__ + if hasattr(instance, '_initial_data["date_created"]'): + instance._initial_data["date_created"] = instance._initial_data["date_created"].strftime("%m/%d/%Y, %H:%M:%S") + if hasattr(instance, 'date_created'): + instance.date_created = instance.date_created.strftime("%m/%d/%Y, %H:%M:%S") + if hasattr(instance, 'date_modified'): + instance.date_modified = instance.date_modified.strftime("%m/%d/%Y, %H:%M:%S") + instance = model_to_dict(instance) + deleted_object_json = json.dumps(instance, cls=DjangoJSONEncoder) + DeletedObject.objects.create(model_name=model_type, object_data=deleted_object_json) + except Exception as e: + _logger.error("cannot save Deleted Object. Discarding..") + _logger.debug(f"error: {e}") \ No newline at end of file diff --git a/src/metax_api/signals/request_finished.py b/src/metax_api/signals/request_finished.py new file mode 100644 index 00000000..29020c8d --- /dev/null +++ b/src/metax_api/signals/request_finished.py @@ -0,0 +1,10 @@ +from django.core.signals import request_finished +from django.dispatch import receiver + +from metax_api.services.rabbitmq_service import RabbitMQService + + +@receiver(request_finished) +def handle_exceptions(sender, **kwargs): + # Request has already been sent back so stuff done here are not prolonging the request handling + RabbitMQService.consume_api_errors() diff --git a/swagger/README.md b/src/metax_api/swagger/README.md similarity index 100% rename from swagger/README.md rename to src/metax_api/swagger/README.md diff --git a/swagger/swagger-yaml-to-html.py b/src/metax_api/swagger/swagger-yaml-to-html.py similarity index 95% rename from swagger/swagger-yaml-to-html.py rename to src/metax_api/swagger/swagger-yaml-to-html.py index 287201fa..359f575d 100755 --- a/swagger/swagger-yaml-to-html.py +++ b/src/metax_api/swagger/swagger-yaml-to-html.py @@ -35,6 +35,8 @@ + + Swagger UI diff --git a/swagger/v1/swagger.yaml b/src/metax_api/swagger/v1/swagger.yaml similarity index 98% rename from swagger/v1/swagger.yaml rename to src/metax_api/swagger/v1/swagger.yaml index f230d356..9c76ecc8 100755 --- a/swagger/v1/swagger.yaml +++ b/src/metax_api/swagger/v1/swagger.yaml @@ -953,10 +953,6 @@ paths: description: A specific filter that targets the following fields; research_dataset['title'], research_dataset['curator'][n]['name'], contract['contract_json']['title']. Restricted to permitted users. required: false type: string - - name: editor - in: query - description: Identifier of the editor used to modify the record, i.e. qvain. - type: string - name: data_catalog in: query description: Filter by data catalog urn identifier @@ -980,15 +976,18 @@ paths: - name: actor_filter in: query description: | - Actor_filters are a collection of filter parameters for filtering according to the name + Actor_filters are a collection of filter parameters for filtering according to the name or organizational/person ID of creator, curator, publisher or rights_holder actors. Actor type must be defined as a suffix in the filter name ('_person' or '_organization'). Actor type '_organization' finds matches from "is_member_of" -field if actor is a person. Multiple actor_filters can be applied simultaneously (AND) - or separately (OR) by using 'condition_separator'. Default separator is AND. + or separately (OR) by using 'condition_separator'. Default separator is AND. If filtered by organizational/person ID, + search needs to be made using complete IDs. Complete person IDs consist of 19 characters matching the following pattern: 0000-0002-1825-0097. + Complete organizational IDs consist of at least 5 characters, the 5 first characters always being numbers (e.g. 00170 or 01901-H960). Examples: '?creator_person=person_name' + '?creator_organization=organizational_id' '?publisher_organization=some organisation&creator_person=person_name&condition_separator=or' required: false type: string @@ -997,6 +996,11 @@ paths: description: Returns datasets that can be edited with certain api version. Possible values are 1 and 2 type: integer required: false + - name: projects + in: query + description: Filter datasets with comma-separated list of IDA projects + required: false + type: string - $ref: "#/parameters/fields" - $ref: "#/parameters/include_legacy" responses: @@ -1625,11 +1629,11 @@ paths: /rpc/datasets/get_minimal_dataset_template: get: summary: Get minimal dataset template. - description: Get minimal dataset template that can be used to create datasets. Service and End Users have different kind of templates. The returned template can be used as-is when creating a dataset into Metax. + description: Get minimal dataset template that can be used to create datasets. Service and End Users have different kind of templates. It is also possible to fetch PAS templates for Service and End Users. The returned template can be used as-is when creating a dataset into Metax. parameters: - name: type in: query - description: Type of dataset template to retrieve. Accepted values: service, enduser. + description: Type of dataset template to retrieve. Accepted values: service, enduser, service_pas, enduser_pas. required: true type: string responses: diff --git a/swagger/v2/swagger.yaml b/src/metax_api/swagger/v2/swagger.yaml similarity index 99% rename from swagger/v2/swagger.yaml rename to src/metax_api/swagger/v2/swagger.yaml index 29060a1d..682ffdf8 100755 --- a/swagger/v2/swagger.yaml +++ b/src/metax_api/swagger/v2/swagger.yaml @@ -958,10 +958,6 @@ paths: description: A specific filter that targets the following fields; research_dataset['title'], research_dataset['curator'][n]['name'], contract['contract_json']['title']. Restricted to permitted users. required: false type: string - - name: editor - in: query - description: Identifier of the editor used to modify the record, i.e. qvain. - type: string - name: data_catalog in: query description: Filter by data catalog urn identifier @@ -985,15 +981,18 @@ paths: - name: actor_filter in: query description: | - Actor_filters are a collection of filter parameters for filtering according to the name + Actor_filters are a collection of filter parameters for filtering according to the name or organizational/person ID of creator, curator, publisher or rights_holder actors. Actor type must be defined as a suffix in the filter name ('_person' or '_organization'). Actor type '_organization' finds matches from "is_member_of" -field if actor is a person. Multiple actor_filters can be applied simultaneously (AND) - or separately (OR) by using 'condition_separator'. Default separator is AND. + or separately (OR) by using 'condition_separator'. Default separator is AND. If filtered by organizational/person ID, + search needs to be made using complete IDs. Complete person IDs consist of 19 characters matching the following pattern: 0000-0002-1825-0097. + Complete organizational IDs consist of at least 5 characters, the 5 first characters always being numbers (e.g. 00170 or 01901-H960). Examples: '?creator_person=person_name' + '?creator_organization=organizational_id' '?publisher_organization=some organisation&creator_person=person_name&condition_separator=or' required: false type: string @@ -1814,11 +1813,11 @@ paths: /rpc/v2/datasets/get_minimal_dataset_template: get: summary: Get minimal dataset template. - description: Get minimal dataset template that can be used to create datasets. Service and End Users have different kind of templates. The returned template can be used as-is when creating a dataset into Metax. + description: Get minimal dataset template that can be used to create datasets. Service and End Users have different kind of templates. It is also possible to fetch PAS templates for Service and End Users. The returned template can be used as-is when creating a dataset into Metax. parameters: - name: type in: query - description: Type of dataset template to retrieve. Accepted values: service, enduser. + description: Type of dataset template to retrieve. Accepted values: service, enduser, service_pas, enduser_pas. required: true type: string responses: diff --git a/src/metax_api/tasks/refdata/refdata_indexer/domain/indexable_data.py b/src/metax_api/tasks/refdata/refdata_indexer/domain/indexable_data.py index 7fd35581..2f75158c 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/domain/indexable_data.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/domain/indexable_data.py @@ -17,9 +17,7 @@ def __init__(self, doc_id, doc_type, label, uri, same_as, scheme): self.doc_type = doc_type self.doc_id = self._create_es_document_id(doc_id) - self.label = ( - label # { 'fi': 'value1', 'en': 'value2',..., 'und': 'default_value' } - ) + self.label = label # { 'fi': 'value1', 'en': 'value2',..., 'und': 'default_value' } self.same_as = same_as self.code = doc_id diff --git a/src/metax_api/tasks/refdata/refdata_indexer/domain/organization_data.py b/src/metax_api/tasks/refdata/refdata_indexer/domain/organization_data.py index d2268f78..082da01a 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/domain/organization_data.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/domain/organization_data.py @@ -12,9 +12,7 @@ class OrganizationData(IndexableData): """ ORG_PURL_BASE_URL = ( - "http://uri.suomi.fi/codelist/fairdata/" - + IndexableData.DATA_TYPE_ORGANIZATION - + "/code/" + "http://uri.suomi.fi/codelist/fairdata/" + IndexableData.DATA_TYPE_ORGANIZATION + "/code/" ) def __init__(self, org_id, label, parent_id="", same_as=[], org_csc="", scheme=""): diff --git a/src/metax_api/tasks/refdata/refdata_indexer/domain/reference_data.py b/src/metax_api/tasks/refdata/refdata_indexer/domain/reference_data.py index 132904fc..7de9c5d6 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/domain/reference_data.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/domain/reference_data.py @@ -77,9 +77,7 @@ def __init__( internal_code="", ): - super(ReferenceData, self).__init__( - data_id, data_type, label, uri, same_as, scheme - ) + super(ReferenceData, self).__init__(data_id, data_type, label, uri, same_as, scheme) self.wkt = wkt self.input_file_format = input_file_format diff --git a/src/metax_api/tasks/refdata/refdata_indexer/es_index_data.py b/src/metax_api/tasks/refdata/refdata_indexer/es_index_data.py index 64e8d85f..9876bb4c 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/es_index_data.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/es_index_data.py @@ -5,7 +5,9 @@ from metax_api.tasks.refdata.refdata_indexer.domain.indexable_data import IndexableData as IdxData from metax_api.tasks.refdata.refdata_indexer.domain.reference_data import ReferenceData as RefData -from metax_api.tasks.refdata.refdata_indexer.service.elasticsearch_service import ElasticSearchService as ESS +from metax_api.tasks.refdata.refdata_indexer.service.elasticsearch_service import ( + ElasticSearchService as ESS, +) from metax_api.tasks.refdata.refdata_indexer.service.finto_data_service import FintoDataService # from service.infra_data_service import InfraDataService @@ -58,9 +60,7 @@ def index_data(): for data_type in RefData.FINTO_REF_DATA_TYPES: finto_es_data_models = finto_service.get_data(data_type) if len(finto_es_data_models) == 0: - _logger.info( - "No data models to reindex for finto data type {0}".format(data_type) - ) + _logger.info("No data models to reindex for finto data type {0}".format(data_type)) continue es.delete_and_update_indexable_data( diff --git a/src/metax_api/tasks/refdata/refdata_indexer/organization_csv_parser.py b/src/metax_api/tasks/refdata/refdata_indexer/organization_csv_parser.py index 5a174e55..7aa6d56c 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/organization_csv_parser.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/organization_csv_parser.py @@ -62,9 +62,7 @@ def parse_csv(): # otherwise create an org and append it to existing root's hierarchy if unit_sub_code and unit_name: - organization_code = "-".join( - [org_code, unit_sub_code] - ) # Unique + organization_code = "-".join([org_code, unit_sub_code]) # Unique parent_id = root_orgs.get(org_code, None) output_orgs.append( create_organization( diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_format_version.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_format_version.json index a7140c51..b0af37db 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_format_version.json +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/file_format_version.json @@ -3,7 +3,6 @@ {"id": "file_format_version_application_epub+zip_2.0.1", "input_file_format": "application/epub+zip", "output_format_version": "2.0.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_epub+zip_2.0.1"}, {"id": "file_format_version_application_epub+zip_3.0.0", "input_file_format": "application/epub+zip", "output_format_version": "3.0.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_epub+zip_3.0.0"}, {"id": "file_format_version_application_epub+zip_3.0.1", "input_file_format": "application/epub+zip", "output_format_version": "3.0.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_epub+zip_3.0.1"}, - {"id": "file_format_version_application_epub+zip_3.1", "input_file_format": "application/epub+zip", "output_format_version": "3.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_epub+zip_3.1"}, {"id": "file_format_version_application_xhtml+xml_1.0", "input_file_format": "application/xhtml+xml", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_xhtml+xml_1.0"}, {"id": "file_format_version_application_xhtml+xml_1.1", "input_file_format": "application/xhtml+xml", "output_format_version": "1.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_xhtml+xml_1.1"}, {"id": "file_format_version_text_xml_1.0", "input_file_format": "text/xml", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/text_xml_1.0"}, @@ -51,36 +50,11 @@ {"id": "file_format_version_image_jp2", "input_file_format": "image/jp2", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_jp2"}, {"id": "file_format_version_image_tiff_6.0", "input_file_format": "image/tiff", "output_format_version": "6.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_tiff_6.0"}, {"id": "file_format_version_image_png_1.2", "input_file_format": "image/png", "output_format_version": "1.2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_png_1.2"}, - {"id": "file_format_version_application_x-internet-archive_1.0", "input_file_format": "application/x-internet-archive", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_x-internet-archive_1.0"}, - {"id": "file_format_version_application_x-internet-archive_1.1", "input_file_format": "application/x-internet-archive", "output_format_version": "1.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_x-internet-archive_1.1"}, - {"id": "file_format_version_application_warc_0.17", "input_file_format": "application/warc", "output_format_version": "0.17", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_warc_0.17"}, {"id": "file_format_version_application_warc_1.0", "input_file_format": "application/warc", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_warc_1.0"}, {"id": "file_format_version_image_tiff_1.0", "input_file_format": "image/tiff", "output_format_version": "1.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_tiff_1.0"}, {"id": "file_format_version_application_gml+xml_3.2.1", "input_file_format": "application/gml+xml", "output_format_version": "3.2.1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_gml+xml_3.2.1"}, {"id": "file_format_version_application_vnd.google-earth.kml+xml_2.3", "input_file_format": "application/vnd.google-earth.kml+xml", "output_format_version": "2.3", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.google-earth.kml+xml_2.3"}, {"id": "file_format_version_application_x-spss-por", "input_file_format": "application/x-spss-por", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_x-spss-por"}, - {"id": "file_format_version_application_msword_8.0", "input_file_format": "application/msword", "output_format_version": "8.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_msword_8.0"}, - {"id": "file_format_version_application_msword_8.5", "input_file_format": "application/msword", "output_format_version": "8.5", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_msword_8.5"}, - {"id": "file_format_version_application_msword_9.0", "input_file_format": "application/msword", "output_format_version": "9.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_msword_9.0"}, - {"id": "file_format_version_application_msword_10.0", "input_file_format": "application/msword", "output_format_version": "10.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_msword_10.0"}, - {"id": "file_format_version_application_msword_11.0", "input_file_format": "application/msword", "output_format_version": "11.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_msword_11.0"}, - {"id": "file_format_version_application_vnd.openxmlformats-officedocument.wordprocessingml.document_12.0", "input_file_format": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "output_format_version": "12.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.wordprocessingml.document_12.0"}, - {"id": "file_format_version_application_vnd.openxmlformats-officedocument.wordprocessingml.document_14.0", "input_file_format": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "output_format_version": "14.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.wordprocessingml.document_14.0"}, - {"id": "file_format_version_application_vnd.openxmlformats-officedocument.wordprocessingml.document_15.0", "input_file_format": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "output_format_version": "15.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.wordprocessingml.document_15.0"}, - {"id": "file_format_version_application_vnd.ms-excel_8.0", "input_file_format": "application/vnd.ms-excel", "output_format_version": "8.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-excel_8.0"}, - {"id": "file_format_version_application_vnd.ms-excel_9.0", "input_file_format": "application/vnd.ms-excel", "output_format_version": "9.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-excel_9.0"}, - {"id": "file_format_version_application_vnd.ms-excel_10.0", "input_file_format": "application/vnd.ms-excel", "output_format_version": "10.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-excel_10.0"}, - {"id": "file_format_version_application_vnd.ms-excel_11.0", "input_file_format": "application/vnd.ms-excel", "output_format_version": "11.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-excel_11.0"}, - {"id": "file_format_version_application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_12.0", "input_file_format": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "output_format_version": "12.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_12.0"}, - {"id": "file_format_version_application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_14.0", "input_file_format": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "output_format_version": "14.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_14.0"}, - {"id": "file_format_version_application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_15.0", "input_file_format": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "output_format_version": "15.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.spreadsheetml.sheet_15.0"}, - {"id": "file_format_version_application_vnd.ms-powerpoint_8.0", "input_file_format": "application/vnd.ms-powerpoint", "output_format_version": "8.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-powerpoint_8.0"}, - {"id": "file_format_version_application_vnd.ms-powerpoint_9.0", "input_file_format": "application/vnd.ms-powerpoint", "output_format_version": "9.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-powerpoint_9.0"}, - {"id": "file_format_version_application_vnd.ms-powerpoint_10.0", "input_file_format": "application/vnd.ms-powerpoint", "output_format_version": "10.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-powerpoint_10.0"}, - {"id": "file_format_version_application_vnd.ms-powerpoint_11.0", "input_file_format": "application/vnd.ms-powerpoint", "output_format_version": "11.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.ms-powerpoint_11.0"}, - {"id": "file_format_version_application_vnd.openxmlformats-officedocument.presentationml.presentation_12.0", "input_file_format": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "output_format_version": "12.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.presentationml.presentation_12.0"}, - {"id": "file_format_version_application_vnd.openxmlformats-officedocument.presentationml.presentation_14.0", "input_file_format": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "output_format_version": "14.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.presentationml.presentation_14.0"}, - {"id": "file_format_version_application_vnd.openxmlformats-officedocument.presentationml.presentation_15.0", "input_file_format": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "output_format_version": "15.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_vnd.openxmlformats-officedocument.presentationml.presentation_15.0"}, {"id": "file_format_version_application_pdf_1.2", "input_file_format": "application/pdf", "output_format_version": "1.2", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.2"}, {"id": "file_format_version_application_pdf_1.3", "input_file_format": "application/pdf", "output_format_version": "1.3", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.3"}, {"id": "file_format_version_application_pdf_1.4", "input_file_format": "application/pdf", "output_format_version": "1.4", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.4"}, @@ -88,7 +62,6 @@ {"id": "file_format_version_application_pdf_1.6", "input_file_format": "application/pdf", "output_format_version": "1.6", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.6"}, {"id": "file_format_version_application_pdf_1.7", "input_file_format": "application/pdf", "output_format_version": "1.7", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_pdf_1.7"}, {"id": "file_format_version_audio_x-aiff", "input_file_format": "audio/x-aiff", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_x-aiff"}, - {"id": "file_format_version_audio_mpeg", "input_file_format": "audio/mpeg", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_mpeg"}, {"id": "file_format_version_audio_x-ms-wma_9", "input_file_format": "audio/x-ms-wma", "output_format_version": "9", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/audio_x-ms-wma_9"}, {"id": "file_format_version_video_dv", "input_file_format": "video/dv", "output_format_version": "", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/video_dv"}, {"id": "file_format_version_video_mpeg_1", "input_file_format": "video/mpeg", "output_format_version": "1", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/video_mpeg_1"}, @@ -96,5 +69,47 @@ {"id": "file_format_version_video_x-ms-wmv_9", "input_file_format": "video/x-ms-wmv", "output_format_version": "9", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/video_x-ms-wmv_9"}, {"id": "file_format_version_application_postscript_3.0", "input_file_format": "application/postscript", "output_format_version": "3.0", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/application_postscript_3.0"}, {"id": "file_format_version_image_gif_1987a", "input_file_format": "image/gif", "output_format_version": "1987a", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_gif_1987a"}, - {"id": "file_format_version_image_gif_1989a", "input_file_format": "image/gif", "output_format_version": "1989a", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_gif_1989a"} + {"id": "file_format_version_image_gif_1989a", "input_file_format": "image/gif", "output_format_version": "1989a", "uri": "http://uri.suomi.fi/codelist/fairdata/file_format_version/code/image_gif_1989a"}, + {"mimetype": "application/epub+zip", "version": "3.2"}, + {"mimetype": "application/matlab", "version": "7"}, + {"mimetype": "application/matlab", "version": "7.3"}, + {"mimetype": "application/msword", "version": "97-2003"}, + {"mimetype": "application/mxf", "version": ""}, + {"mimetype": "application/vnd.ms-excel", "version": "8"}, + {"mimetype": "application/vnd.ms-excel", "version": "8X"}, + {"mimetype": "application/vnd.ms-powerpoint", "version": "97-2003"}, + {"mimetype": "application/vnd.oasis.opendocument.formula", "version": "1.3"}, + {"mimetype": "application/vnd.oasis.opendocument.graphics", "version": "1.3"}, + {"mimetype": "application/vnd.oasis.opendocument.presentation", "version": "1.3"}, + {"mimetype": "application/vnd.oasis.opendocument.spreadsheet", "version": "1.3"}, + {"mimetype": "application/vnd.oasis.opendocument.text", "version": "1.3"}, + {"mimetype": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "version": "2007 onwards"}, + {"mimetype": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "version": "2007 onwards"}, + {"mimetype": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "version": "2007 onwards"}, + {"mimetype": "application/x-hdf5", "version": "1.10"}, + {"mimetype": "application/x-siard", "version": "2.0"}, + {"mimetype": "application/x-siard", "version": "2.1"}, + {"mimetype": "application/xhtml+xml", "version": "5.0"}, + {"mimetype": "audio/L16", "version": ""}, + {"mimetype": "audio/L20", "version": ""}, + {"mimetype": "audio/L24", "version": ""}, + {"mimetype": "audio/L8", "version": ""}, + {"mimetype": "audio/mpeg", "version": "1"}, + {"mimetype": "audio/mpeg", "version": "2"}, + {"mimetype": "image/svg+xml", "version": "1.1"}, + {"mimetype": "image/tiff", "version": "1.5"}, + {"mimetype": "text/html", "version": "5.0"}, + {"mimetype": "text/html", "version": "5.1"}, + {"mimetype": "text/html", "version": "5.2"}, + {"mimetype": "text/xml", "version": "1.1"}, + {"mimetype": "video/avi", "version": ""}, + {"mimetype": "video/mj2", "version": ""}, + {"mimetype": "video/MP1S", "version": ""}, + {"mimetype": "video/MP2P", "version": ""}, + {"mimetype": "video/MP2T", "version": ""}, + {"mimetype": "video/MP2T", "version": ""}, + {"mimetype": "video/quicktime", "version": ""}, + {"mimetype": "video/x-ffv", "version": "3"}, + {"mimetype": "video/x-matroska", "version": "4"}, + {"mimetype": "video/x-ms-asf", "version": ""} ] diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv b/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv index 933af772..0442f975 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv @@ -1,90 +1,110 @@ org_name_fi,org_name_en,org_name_sv,org_code,unit_main_code,unit_sub_code,unit_name,org_isni,org_csc -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,,,http://isni.org/isni/0000000108389418,2 -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A800,"School services, ARTS",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A801,"Department of Film, Television and Scenography",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A802,Department of Media,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A803,Department of Design,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A805,Department of Art,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A850,Aalto Future Media Center,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,A899,School Common ARTS,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E700,"School Services, BIZ",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E701,Department of Accounting,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E702,Department of Marketing,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E703,Department of Economics,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E704,Department of Information and Service Economy,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E706,Department of Management Studies,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E707,Department of Finance,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E710,Center for Markets in Transition (CEMAT),, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E720,Center for Knowledge and Innovation Research (CKIR),, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,E790,"School Common, BIZ",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T100,"School services, CHEM",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T101,Department of Biotechnology and Chemical Technology,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T102,Department of Chemistry,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T103,Department of Materials Science and Engineering,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T104,Department of Forest Products Technology,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T105,Department of Chemistry and Materials Science,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T106,Department of Chemical and Metallurgical Engineering,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T107,Department of Bioproducts and Biosystems,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T199,"Shool common, CHEM",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T200,"School cervices, ENG",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T201,Department of Architecture,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T210,Design Factory,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T211,"School common, ENG",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T212,Department of Mechanical Engineering,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T213,Department of Built Environment,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T214,Department of Civil Engineering,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T300,School services SCI,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T302,Department of Mathematics and Systems Analysis,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T304,Department of Applied Physics,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T307,Department of Industrial Engineering and Management,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T310,EIT Digital Helsinki,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T313,Department of Computer Science,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T314,Department of Neuroscience and Biomedical Engineering,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T320,"School common, SCI",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T400,School services ELEC,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T403,Deptartment of Micro & Nanoscience,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T404,Department of Radio Science & Engineering,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T405,Department of Signal Processing & Acoustics,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T407,Department of Communications & Networking,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T408,Metsähovi Radio Observatory,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T409,Aalto Nanofab,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T410,Department of Electrical Engineering & Automation,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T411,Department of Electronics & Nanoengineering,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,T499,"School common, ELEC",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U100,University level,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U900,"Leadership and LS Support Serv, Joint Units",, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U901,Research and Innovation Serv,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U902,Learning Services,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U905,Finance Services,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U906,HR Services,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U908,Communications Services,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U909,IT Services,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U912,Campus Services,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U915,Secretarial Services,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U917,Fundraising,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U918,Endowment,, -Aalto yliopisto,Aalto University,Aalto universitetet,10076,,U920,Aalto Common Items,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,,,http://isni.org/isni/0000000110165683,3556 -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Liiktal,Liiketalouden koulutus,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Kemtek,Kemiantekniikan koulutus,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Tiettek,Tietotekniikan koulutus,, +Aalto-yliopisto,Aalto University,,10076,,,,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,A855,ARTS Infra,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U920,Aalto Common Items,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T409,Aalto Nanofab,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,A850,Aalto Studios,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U917,Advancement and Corporate Engagament Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U912,Campus Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,E720,Center for Knowledge and Innovation Research (CKIR,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,E710,Center for Markets in Transition (CEMAT),, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U908,Communications Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T407,Department of Communications & Networking,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,E701,Department of Accounting,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T304,Department of Applied Physics,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T201,Department of Architecture,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,A805,Department of Art,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T107,Department of Bioproducts and Biosystems,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T101,Department of Biotechnology and Chemical Technolog,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T213,Department of Built Environment,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T106,Department of Chemical and Metallurgical Engineeri,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T102,Department of Chemistry,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T105,Department of Chemistry and Materials Science,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T214,Department of Civil Engineering,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T313,Department of Computer Science,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,A803,Department of Design,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,E703,Department of Economics,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T410,Department of Electrical Engineering & Automation,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T411,Department of Electronics & Nanoengineering,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,A801,"Department of Film, Television and Scenography",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,E707,Department of Finance,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T104,Department of Forest Products Technology,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T307,Department of Industrial Engineering and Managemen,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,E704,Department of Information and Service Management,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,E706,Department of Management Studies,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,E702,Department of Marketing,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T103,Department of Materials Science and Engineering,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T302,Department of Mathematics and Systems Analysis,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T212,Department of Mechanical Engineering,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,A802,Department of Media,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T314,Department of Neuroscience and Biomedical Engineer,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T404,Department of Radio Science & Engineering,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T405,Department of Signal Processing & Acoustics,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T403,Deptartment of Micro & Nanoscience,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T210,Design Factory,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T310,EIT Digital Helsinki,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U918,Endowment,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U300,Endowment,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U905,Finance Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U906,HR Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U909,IT Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U924,Innovation Ecosystem Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U410,Joint Learning,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U420,Joint entrepreneurship act.,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U926,Language Center,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U900,"Leadership and Leadership Support Services, Joint",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U902,Learning Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U925,Legal Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T408,Metsähovi Radio Observatory,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U400,OtaNano,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U901,Research Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,A899,School Common ARTS,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,E790,"School Common, BIZ",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,E700,"School Services, BIZ",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T200,"School cervices, ENG",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T499,"School common, ELEC",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T211,"School common, ENG",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T320,"School common, SCI",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,ARTS,"School of Arts, Design and Architecture",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,BIZ,School of Business,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,CHEM,School of Chemical Engineering,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,ELEC,School of Electrical Engineering,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,ENG,School of Engineering,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,SCI,School of Science,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T400,School services ELEC,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T300,School services SCI,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,A800,"School services, ARTS",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T100,"School services, CHEM",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U915,Secretarial Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U919,Security and Lobby Services,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,T199,"Shool common, CHEM",, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U200,U JSI,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U100,University level,, +Aalto-yliopisto,Aalto University,Aalto-universitetet,10076,,U,University level and joint,, +Alkoholitutkimussäätiö,The Finnish Foundation for Alcohol Studies,,02009295,,,,, +CSC - Tieteen tietotekniikan keskus Oy,CSC – IT Center for Science,,09206320,,,,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,,,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Huma,Humanistisen ja kasvatusalan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Kemtek,Kemiantekniikan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Korkpal,Korkeakoulupalvelut,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Liiktal,Liiketalouden koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Musa,Musiikin koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Sosia,Sosiaalialan koulutus,, Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Sahtek,Sähkö- ja automaatiotekniikan koulutus,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Terva,Terveysalan koulutus,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Sosia,Sosiaalialan koulutus,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Musa,Musiikin koulutus,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Huma,Humanistisen ja kasvatusalan koulutus,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,YAMK,Ylemmät AMK-tutkinnot,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Korkpal,Korkeakoulupalvelut,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,TKIpal,TKI-palvelut,, -Centria-ammattikorkeakoulu,Centria University of Applied Sciences,,02536,,Tuotal,Tuotantotalouden koulutus,, -Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,,,http://isni.org/isni/0000000088808274,367 +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Sähtek,Sähkö- ja automaatiotekniikan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Sähtek,Sähkö- ja automaatiotekniikan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,TKIpal,TKI-palvelut,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Terva,Terveysalan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Tiettek,Tietotekniikan koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,Tuotal,Tuotantotalouden koulutus,, +Centria-ammattikorkeakoulu,Centria University of Applied Sciences,Centria-ammattikorkeakoulu,02536,,YAMK,Ylemmät AMK-tutkinnot,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,,,, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,11,11 Yleinen ryhmä,, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,12,"12 Communication, Communities and Languages",, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,13,13 Opiskelijoiden ohjaus ja kasvu,, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,14,14 Pedagogical and community development,, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,21,21 Yleinen ryhmä,, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,22,22 Aikuis- ja työikäisen väestön kanssa tehtävä työ,, -Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,23,Alusta DIGI-USER,, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,24,24 Hoitotyön ja terveyden edistäminen,, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,26,"26 Lapsi-, nuoriso- ja perhetyö",, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,31,31 Yleinen ryhmä,, @@ -94,2347 +114,3848 @@ Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,41,4 Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,42,42 TKI-opinnot ja -toiminta,, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,43,"43 Ylemmät tutkinnot, työkäytännöt ja johtamistoiminta",, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,51,51 Yleinen ryhmä,, -Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,61,61 Rehtoraatti,, -Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,62,"62 Viestintä, tiedotus ja markkinointi",, -Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,63,63 Henkilöstöpalvelut,, -Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,64,64 Talouspalvelut,, -Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,65,65 Tietohallintopalvelut,, -Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,66,66 Kirjasto ja tietopalvelut,, -Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,68,68 Tilapalvelut,, -Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,74,"74 Opintotoimistopalvelut, hakutoimisto",, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,75,75 Kansainvälisyyspalvelut,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,,,http://isni.org/isni/0000000406476405,570 -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,AOKK,Ammatillinen opettajankoulutus,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,JOURA,Journalismin ko,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,ASSI,Johdon assistenttityön ja kielten ko.,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,FINA,Finanssi- ja talousasiantuntijan koulutusohjelma,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,GLOBBA,DP in International Business,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HELI,Liiketalouden ko,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,LIIPO,Liiketalouden ko,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,,02623,,23,Alusta DIGI-USER,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,65,Digipalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,87,Hanketoiminta;;,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,63,Henkilostopalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,68,Infrapalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,84,Innovaatiotoiminta;;,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,67,Kampuspalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,88,Kansainvalinen toiminta,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,66,Kirjasto ja tietopalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,96,Kirkolliset opinnot,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,91,Koulutus,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,85,Liiketoiminta ja innovaatiot;;,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,74,Opiskelijapalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,86,Pedagogiikka ja kehittaminen;;,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,61,Rehtoraatti,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,92,Sairaanhoitaja- ja terveydenhoitajatutkinnot (temk,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,93,Sairaanhoitaja- ja terveydenhoitajatutkinnot (temk,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,94,Sosionomitutkinnot (temkok 1-3),, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,95,"Sosionomitutkinnot (temkok 4-7, DSS)",, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,64,Talouspalvelut,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,97,Tulkkaustutkinnot ja kielet,, +Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,62,"Viestinta, tiedotus ja markkinointi",, +Geologian tutkimuskeskus,Geological Survey of Finland,,5040011,,,,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030014,Alueellinen geotieto ALG,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030016,Digitaaliset tuotteet ja palvelut DIP,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040200350,Digitaalisuus ja tietovarannot,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300311,Energia ja rakentamisen ratkaisut,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030001,Geoenergia GNR,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300361,Geofysiikan ratkaisut,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030012,Geofysiikan sovellukset GSO,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030015,Geotietovarannon hallinta GEH,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040200510,"Henkilöstö, osaaminen ja lakiasiat",, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200310,"Henkilöstö, osaaminen ja työympäristöt",, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030003,Kalliorakentaminen ja sijoituspaikat KAS,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300355,"Kiertotalouden ratkaisut, Espoon tutkimuslaboratorio",, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300356,"Kiertotalouden ratkaisut, Kaivosympäristöt ja sivuvirrat",, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300352,"Kiertotalouden ratkaisut, Oku koetehdas",, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300353,"Kiertotalouden ratkaisut, Oku rikastuslaboratorio",, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300354,"Kiertotalouden ratkaisut, Oku rikastusmineralogia",, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300351,"Kiertotalouden ratkaisut, Yhteiset",, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300171,Malmit ja teollisuusmineraalit MTM,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030005,Merigeologia MRG,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300341,Mineraalitalouden ratkaisut,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030006,Mineraalitalous ja malmigeologia MIM,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030009,Mineraalitekniikka ja materiaalit MMA,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030007,Mineraalivarannot MIV,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030011,Pohjavesi PVI,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040200330,Strategia ja suunnittelu,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040200710,Strategia ja toiminnan tuki,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040200,50402003,Strateginen johto,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030008,Teollisuusmineraalit TMI,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040200340,Tieteellinen tutkimus,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040200610,Tieteellinen tutkimus,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300301,Tietoratkaisut,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040200,5040200,Toiminnan johtaminen,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,5040300,Tulosyksiköt,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030013,Tuotantoympäristöt ja kierrätys TUK,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030010,Turvevarannot TUR,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300321,Vesiratkaisut,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040200320,Viestintä ja markkinointi,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040200420,Viestintä ja markkinointi,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300018,Yhdyskunnat ja geoenergia YGE,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030002,Yhdyskunnat ja rakentaminen YRA,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030004,Ympäristögeologia YMP,, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300332,"Ympäristöratkaisut, Merigeologia",, +Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,5040300331,"Ympäristöratkaisut, Ympäristö",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,,,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,AOKK,AOKK Ammatillinen opettajakorkeakoulu,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2400,Analytiikka ja kehittäminen,, Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,MUBBA,DP for Multilingual Management Assistants,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,MYYNTI,Myyntityön ko,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,POBBA,DP in International Business,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,SAMPO,DP in International Sales and Marketing,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,VIMA,Myyntityön ja visuaalisen markkinoinnin ko,, Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,ABBA,DP in Aviation Business,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HETI,Tietojenkäsittelyn ko,, Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,BITE,DP in Business Information Technology,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,LOT,Liikunnan ja vapaa-ajan ko,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,SPORT,DP in Sports and Leisure Management,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HOTRA,Hotelli- ja ravintola-alan liikkeenjohdon ko.,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,RUOKA,Hotelli- ja ravintola-alan ko.,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,RUOKAT,Ruokatuotannon johtamisen ko,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,MATKA,Matkailun liikkeenjohdon ko.,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HOSBA,"DP in Hotel, Restaurant and Tourism Management",, Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HOTEM,"DP in Hospitality, Tourism and Experience Management",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HOSBA,"DP in Hotel, Restaurant and Tourism Management",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,GLOBBA,DP in International Business,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,POBBA,DP in International Business,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,SAMPO,DP in International Sales and Marketing,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,SPORT,DP in Sports and Leisure Management,, Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,TOBBA,DP in Tourism,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,DT,"DT-HTP, TKI, Kovat",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5500,Digipalvelut ja tietohallinto,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2410,Digitaaliset palvelut,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,EHTH,"EHTH-HTP, TKI ja Kovat Haagassa",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,EHTP,"EHTP-HTP, TKI ja Kovat Porvoossa",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,EHTV,"EHTV-HTP, TKI ja Kovat Vierumäellä",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5510,Elinkaari- ja käyttäjäpalvelut,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,FINA,Finanssi- ja talousasiantuntijan koulutusohjelma,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2100,HR ja johtaminen,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,HTP,"HTP-Hallinto- ja tukipalvelut (KOPA, HR, Tieto, Ta",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5200,Henkilöstö ja kulttuuri,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,RUOKA,Hotelli- ja ravintola-alan ko.,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HOTRA,Hotelli- ja ravintola-alan liikkeenjohdon ko.,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2420,ICT ja liiketoiminta,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5520,ICT-infra ja pääsynhallinta,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,1400,IT-tradenomi,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2430,Infra ja pilvipalvelut,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,1900,Jatkuva oppiminen,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,ASSI,Johdon assistenttityön ja kielten ko.,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2500,Journalismi,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,JOURA,Journalismin ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2110,Juridiikka,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,4300,Kansainvälisyyspalvelut,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2510,Kielet ja kansainvälisyys,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5420,Kiinteistöt ja turvallisuus,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5120,Koulutuksen palvelut,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,4200,Koulutusvienti,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2000,Kp 2000,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,LT,LT-Kovat ja muut (ei opetus);;,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,LTM,LT-Tradenomi-opetus Malmilla,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,LTP,LT-Tradenomi-opetus Pasilassa,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HELI,Liiketalouden ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,LIIPO,Liiketalouden ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,LOT,Liikunnan ja vapaa-ajan ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,1600,Liikunnanohjaaja,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2600,Liikunta ja hyvinvointi,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2200,Majoitus- ja ravitsemisliiketoiminta,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2520,Markkinointi ja viestintä,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,1800,Master-tutkinnot,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2210,Matkailuliiketoiminta,, Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,POMO,Matkailun ko,, -Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HTP,Hallinto- ja tukipalvelut,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,,,http://isni.org/isni/0000000404102071,11 -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H01,Yliopistopalvelut,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H10,Teologinen tiedekunta,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H20,Oikeustieteellinen tiedekunta,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H30,Lääketieteellinen tiedekunta,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H40,Humanistinen tiedekunta,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H50,Matemaattis- luonnontieteellinen tiedekunta,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H55,Farmasian tiedekunta,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,MATKA,Matkailun liikkeenjohdon ko.,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,1500,Medianomi,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,3100,Myynnin kehittäminen ja digitalisaatio,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2120,Myynti ja asiakkuudet,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,4100,Myynti ja partneritoiminta,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,VIMA,Myyntityön ja visuaalisen markkinoinnin ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,MYYNTI,Myyntityön ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2440,Ohjelmistotuotanto,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5110,Opinto- ja hakijapalvelut,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,3500,Oppimis- ja tutkimusympäristöt,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,3200,Palveluliiketoiminnan kehittäminen ja muotoilu,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2130,Palveluliiketoiminta,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2700,Pedagogiikka,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,4400,Pedagoginen täydennyskoulutus,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2140,Raha ja talous,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5000,"Rehtori, toimitusjohtaja",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,1200,Restonomi,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,RUOKAT,Ruokatuotannon johtamisen ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,TKI,TKI-Master-tiimi,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5300,Talous,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,,10056,,HETI,Tietojenkäsittelyn ko,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,3650,Tietopalvelut,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2450,Toiminnanohjaus,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5100,Toiminnanohjaus ja laatu,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,1100,Tradenomi,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,1150,Tradenomi ENG,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,3600,Tutkimuspalvelut,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,3610,Ulysseus Eurooppa-yliopisto,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,1000,Ura- ja opinto-ohjaus,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2610,Urheilu,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,3300,Vaikuttava ammatillinen pedagogiikka,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5400,Vastuullisuus ja opiskelijahyvinvointi,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,5410,"Viestintä, markkinointi ja alumnitoiminta",, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,2150,Yrittäjyys,, +Haaga-Helia ammattikorkeakoulu,Haaga-Helia University of Applied Sciences,Haaga-Helia ammattikorkeakoulu,10056,,3400,Yrittäjyys ja liiketoiminnan uudistaminen,, +Helsingin Sanomain Säätiö sr,Helsingin Sanomat Foundation,,20668235,,,,, +Helsingin seudun yliopistollisen keskussairaalan erityisvastuualue,Helsinki University Hospital Catchment Area,,15675350,,,,, +Helsingin yliopisto,University of Helsinki,,01901,,,,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H930,Avoin yliopisto,, Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H57,Bio- ja ympäristötieteellinen tiedekunta,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H60,Käyttäytymistieteellinen tiedekunta,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H70,Valtiotieteellinen tiedekunta,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H74,Svenska social- och kommunalhögskolan,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H80,Maatalous- metsätieteellinen tiedekunta,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H90,Eläinlääketieteellinen tiedekunta,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H903,Tila- ja kiinteistökeskus,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H906,Kielikeskus,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H907,Tietotekniikkakeskus,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H909,UniSport,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H916,Neurotieteen tutkimuskeskus,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H918,Tutkijakollegium,, Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H919,Biotekniikan instituutti,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H92,Helsingin yliopiston tutkijakoulut,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H930,Avoin yliopisto,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H945,Suomen molekyylilääketieteen instituutti,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H90,Eläinlääketieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H55,Farmasian tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H985,Helsingin yliopiston kirjasto,, Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H955,Helsingin yliopiston koe-eläinkeskus,, Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H960,Helsingin yliopiston koulutus- ja kehittämispalvelut,, Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H97,Helsinki Institute of Life Science,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H978,Luonnontieteellinen keskusmuseo,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H40,Humanistinen tiedekunta,, Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H981,Kansalliskirjasto,, -Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H985,Helsingin yliopiston kirjasto,, -Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,,,http://isni.org/isni/000000040624783X,1875 +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H60,Kasvatustieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H906,Kielikeskus,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H978,Luonnontieteellinen keskusmuseo,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H30,Lääketieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H80,Maatalous-metsätieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H50,Matemaattis-luonnontieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H916,Neurotieteen tutkimuskeskus,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H20,Oikeustieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H945,Suomen molekyylilääketieteen instituutti,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H74,Svenska social- och kommunalhögskolan,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H10,Teologinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H907,Tietotekniikkakeskus,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H903,Tila- ja kiinteistökeskus,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H92,Tohtoriohjelmat,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H918,Tutkijakollegium,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H909,Unisport,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H70,Valtiotieteellinen tiedekunta,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H00,Yliopiston johto ja yhteiset,, +Helsingin yliopisto,University of Helsinki,Helsingfors universitet,01901,,H01,Yliopistopalvelut,, +Humanistinen ammattikorkeakoulu,Humak University of Applied Sciences,,02631,,,,, +Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,30,Humanistiset alat,, +Humanistinen ammattikorkeakoulu,Humak University of Applied Sciences,Humanistinen ammattikorkeakoulu,02631,,400,Innovaatiopalvelut,, +Humanistinen ammattikorkeakoulu,Humak University of Applied Sciences,Humanistinen ammattikorkeakoulu,02631,,300,Kehittämispalvelut,, +Humanistinen ammattikorkeakoulu,Humak University of Applied Sciences,Humanistinen ammattikorkeakoulu,02631,,200,KulTu,, +Humanistinen ammattikorkeakoulu,Humak University of Applied Sciences,Humanistinen ammattikorkeakoulu,02631,,100,Kulttuuri,, Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,10,Taiteet ja kulttuurialat,, Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,20,Terveys- ja hyvinvointialat,, -Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,30,Humanistiset alat,, Humanistinen ammattikorkeakoulu,HUMAK University of Applied Sciences,,02631,,50,Yhteiset,, -Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,,,http://isni.org/isni/0000000106852595,9 -Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,1,Yhteiset palvelut,, -Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,11,Yrittäjyys ja liiketoimintaosaaminen,, -Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,4,Ammatillinen opettajakorkeakoulu,, -Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,6,Hyvinvointiosaaminen,, -Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,7,Teknologiaosaaminen,, -Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,8,Biotalous,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,,,http://isni.org/isni/0000000121143658,SXTDpC1TdW9tZW4geWxpb3Bpc3Rv -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,202010,Kasvatustieteiden ja psykologian osasto / Yhteiset,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,100000,100000,"Yliopiston johto, yhteiset ja yliopistopalvelut",, +Humanistinen ammattikorkeakoulu,Humak University of Applied Sciences,Humanistinen ammattikorkeakoulu,02631,,500,Yhteiset palvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,,02467,,,,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,132,AOKK tukipalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,131,Ammatillinen erityisopettaja- ja opinto-ohjaajakou,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,4,Ammatillinen opettajakorkeakoulu,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,2,Ammatillinen opettajakoulutus,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,60,Ammatillisen opettajakorkeakoulun tyoelamapalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,101,BIOS tukipalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,5,Bio- ja elintarviketekniikka,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,61,Biotalouden tyoelamapalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,8,Biotalouden yksikko,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,102,Biotalousinsinoori,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,48,HAMK Bio,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,89,HAMK Design Factory,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,3,HAMK Edu,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,43,HAMK Smart,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,27,HAMK Tech,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,13,HYOS tukipalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,10,Hallinto-ja talouspalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,12,Henkilostopalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,46,Hoitotyö,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,62,Hyvinvointiosaamisen tyoelamapalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,6,Hyvinvointiosaamisen yksikko,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,14,International Business,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,67,Kansainvaliset palvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,15,Kestava kehitys,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,16,Kiinteistopalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,17,Kirjasto ja tietopalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,18,Konetekniikka,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,44,Koulutuksen tukipalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,37,Liikenneala,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,20,Liiketalous,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,21,Maaseutuelinkeinot ja hevostalous,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,25,Metsatalous,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,26,Muotoilu,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,29,Puutarhatalous,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,30,Rakennettu ymparisto,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,31,Rakennus- ja yhdyskuntatekniikka,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,33,Sahko- ja automaatiotekniikka,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,32,Sosiaaliala,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,40,Strateginen viestinta,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,34,TEOS tukipalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,65,Teknologiaosaamisen tyoelamapalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,7,Teknologiaosaamisen yksikko,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,35,Tieto- ja viestintatekniikka,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,36,Tietohallinto,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,38,Tietojenkasittely,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,42,YRLI tukipalvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,1,Yhteiset palvelut,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,41,Yleishallinto,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,11,Yrittajyyden ja liiketoimintaosaamisen yksikko,, +Hämeen ammattikorkeakoulu,Häme University of Applied Sciences,Hämeen ammattikorkeakoulu,02467,,66,Yrittajyyden tyoelamapalvelut,, +Ilmatieteen laitos,Finnish Meteorological Insitute,Meterologiska institutet,02446647,,,,http://isni.org/isni/0000000122538678, +Ilmatieteen laitos,Finnish Meteorological Institute,,4940015,,,,, +Innovaatiorahoituskeskus Business Finland,Innovaatiorahoituskeskus Business Finland,,05126964,,,,, +Itä-Suomen yliopisto,University of Eastern Finland,,10088,,,,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,401010,A.I. Virtanen -instituutti,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,401020,A.I. Virtanen -instituutti / Bioteknologia ja molekulaarinen lääketiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,401030,A.I. Virtanen -instituutti / Neurobiologia,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,401010,A.I. Virtanen -instituutti / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,402010,Farmasian laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,200010,Filosofisen tiedekunnan hallinto,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,200010,Filosofisen tiedekunnan hallinto / Yhteiset,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,201010,Humanistinen osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,303010,Fysiikan ja matematiikan laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,205010,Harjoittelukoulut / Joensuu,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,205020,Harjoittelukoulut / Savonlinna,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,501010,Historia- ja maantieteiden laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,403010,Hoitotieteen laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,2010,Humanistinen osasto,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,201020,Humanistinen osasto / Suomen kieli ja kulttuuritieteet,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,201030,Humanistinen osasto / Vieraat kielet ja käännöstiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,201010,Humanistinen osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,201020,"Humanistinen osasto, suomen kieli ja kulttuuritiet",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,201030,"Humanistinen osasto, vieraat kielet ja käännöst",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,201010,"Humanistinen osasto, yhteiset",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,600000,600000,Itä-Suomen yliopiston apteekki,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,600010,Itä-Suomen yliopiston apteekki,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,2050,Itä-Suomen yliopiston harjoittelukoulu,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,205010,Itä-Suomen yliopiston harjoittelukoulu,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,205020,"Itä-Suomen yliopiston harjoittelukoulu, Savonlinna",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,700000,Jatkuvan oppimisen keskus,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,506010,Karjalan tutkimuslaitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,2020,Kasvatustieteiden ja psykologian osasto,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,202020,Kasvatustieteiden ja psykologian osasto / Erityispedagogiikka,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,202030,"Kasvatustieteiden ja psykologian osasto / Kasvatustiede, aikuiskasvatus ja ohjaus",, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,202040,Kasvatustieteiden ja psykologian osasto / Psykologia,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,203010,Soveltavan kasvatustieteen ja opettajankoulutuksen osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,202010,Kasvatustieteiden ja psykologian osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,202040,"Kasvatustieteiden ja psykologian osasto, Psykologi",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,202020,"Kasvatustieteiden ja psykologian osasto, erityispe",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,202030,"Kasvatustieteiden ja psykologian osasto, kasvatust",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,202010,"Kasvatustieteiden ja psykologian osasto, yhteiset",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,502010,Kauppatieteiden laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,304010,Kemian laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,800010,Kielikeskus,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,800020,Kirjasto,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,405010,Koe-eläinkeskus,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,300010,Luonnontieteiden ja metsätieteiden tiedekunnan ha,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,300010,Luonnontieteiden ja metsätieteiden tiedekunnan hallinto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,4040,Lääketieteen laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404020,Lääketieteen laitos / Biolääketiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404050,Lääketieteen laitos / Hammaslääketiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404030,Lääketieteen laitos / Kansanterveystiede ja kliininen ravitsemustiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404040,Lääketieteen laitos / Kliininen lääketiede,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404010,Lääketieteen laitos / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,404020,"Lääketieteen laitos, biolääketiede",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,404050,"Lääketieteen laitos, hammaslääketiede",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,404030,"Lääketieteen laitos, kansanterveystiede ja kliininen ravitsemustiede",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,404040,"Lääketieteen laitos, kliininen lääketiede",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,404010,"Lääketieteen laitos, yhteiset",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,507010,Matkailualan opetus- ja tutkimuslaitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,309010,Mekrijärven tutkimusasema,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,305010,Metsätieteiden osasto,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,503010,Oikeustieteiden laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,308010,SIB-labs -infrastruktuuriyksikkö,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,504010,Sosiaali- ja terveysjohtamisen laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,303510,Sovelletun fysiikan laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,203010,Soveltavan kasvatustieteen ja opettajankoulutuksen,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,203020,Soveltavan kasvatustieteen ja opettajankoulutuksen osasto,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,2030,Soveltavan kasvatustieteen ja opettajankoulutuksen osasto,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,203020,Soveltavan kasvatustieteen ja opettajankoulutuksen osasto / Joensuu,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,203030,Soveltavan kasvatustieteen ja opettajankoulutuksen osasto / Savonlinna,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,204010,Teologian osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,203010,Soveltavan kasvatustieteen ja opettajankoulutuksen osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,203030,"Soveltavan kasvatustieteen ja opettajankoulutuksen osasto, Savonlinna",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,2040,Teologian osasto,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,204020,Teologian osasto / Läntinen teologia,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,204030,Teologian osasto / Ortodoksinen teologia,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,205010,Harjoittelukoulut / Joensuu,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,205020,Harjoittelukoulut / Savonlinna,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,300010,Luonnontieteiden ja metsätieteiden tiedekunnan hallinto / Yhteiset,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,303010,Fysiikan ja matematiikan laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,303510,Sovelletun fysiikan laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,304010,Kemian laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,305010,Metsätieteiden osasto,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,306020,Tietojenkäsittelytieteen laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,307010,Ympäristö- ja biotieteiden laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,308010,SIB-labs -infrastruktuuriyksikkö,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,300000,309010,Mekrijärven tutkimusasema,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,200000,204010,Teologian osasto / Yhteiset,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,204020,"Teologian osasto, läntinen teologia",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,204030,"Teologian osasto, ortodoksinen teologia",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,204010,"Teologian osasto, yhteiset",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,400010,Terveystieteiden tiedekunnan hallinto,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,400010,Terveystieteiden tiedekunnan hallinto / Yhteiset,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,401010,A.I. Virtanen -instituutti / Yhteiset,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,401020,A.I. Virtanen -instituutti / Bioteknologia ja molekulaarinen lääketiede,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,401030,A.I. Virtanen -instituutti / Neurobiologia,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,402010,Farmasian laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,403010,Hoitotieteen laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404010,Lääketieteen laitos / Yhteiset,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404020,Lääketieteen laitos / Biolääketiede,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404030,Lääketieteen laitos / Kansanterveystiede ja kliininen ravitsemustiede,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404040,Lääketieteen laitos / Kliininen lääketiede,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,404050,Lääketieteen laitos / Hammaslääketiede,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,400000,405010,Koe-eläinkeskus,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,306020,Tietojenkäsittelytieteen laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,500010,Yhteiskuntatieteiden ja kauppatieteiden tiedekunna,, Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,500010,Yhteiskuntatieteiden ja kauppatieteiden tiedekunnan hallinto / Yhteiset,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,501010,Historia- ja maantieteiden laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,502010,Kauppatieteiden laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,503010,Oikeustieteiden laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,504010,Sosiaali- ja terveysjohtamisen laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,505010,Yhteiskuntatieteiden laitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,506010,Karjalan tutkimuslaitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,500000,507010,Matkailualan opetus- ja tutkimuslaitos,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,600000,600000,Itä-Suomen yliopiston apteekki,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,700000,700000,Koulutus- ja kehittämispalvelu Aducate,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,800000,800010,Kielikeskus,, -Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,800000,800020,Kirjasto,, -Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,,,http://isni.org/isni/0000000404148475,Snl2w6Rza3lsw6RuIGFtbWF0dGlrb3JrZWFrb3VsdQ== -Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,1,Ammatillinen opettajakorkeakoulu,, -Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,2,Hyvinvointiyksikkö,, -Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,3,Liiketoimintayksikkö,, -Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,4,Teknologiayksikkö,, -Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,5,Hallintoyksikkö,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,,,http://isni.org/isni/0000000121799569,Snl2w6Rza3lsw6RuIHlsaW9waXN0bw== +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,505010,Yhteiskuntatieteiden laitos,, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,100000,"Yliopiston johto, yhteiset ja yliopistopalvelut",, +Itä-Suomen yliopisto,University of Eastern Finland,Östra Finlands universitet,10088,,307010,Ympäristö- ja biotieteiden laitos,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,,02504,,,,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,Jyväskylän ammattikorkeakoulu,02504,,1,Ammatillinen opettajakorkeakoulu,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,Jyväskylän ammattikorkeakoulu,02504,,5,Hallintoyksikkö,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,Jyväskylän ammattikorkeakoulu,02504,,2,Hyvinvointiyksikkö,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,Jyväskylän ammattikorkeakoulu,02504,,3,Liiketoimintayksikkö,, +Jyväskylän ammattikorkeakoulu,JAMK University of Applied Sciences,Jyväskylän ammattikorkeakoulu,02504,,4,Teknologiayksikkö,, +Jyväskylän yliopisto,University of Jyväskylä,,01906,,,,, Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,20019100,Agora Center,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,216010,Avance Johtamiskoulutus,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219200,Avoimen tiedon keskus,, Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219110,Avoin yliopisto,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219200,Kirjasto,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219120,Kokkolan yliopistokeskus Chydenius,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,20019140,Koulutuksen tutkimuslaitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219230,Tiedemuseo,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219220,Yliopiston kielikeskus,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219700,Johto ja yliopiston yhteiset,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219500,Yliopistopalvelut,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,213000,Humanistinen tiedekunta,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213050,Historian ja etnologian laitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213040,Kielten laitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213010,Musiikin laitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213060,Soveltavan kielentutkimuksen keskus,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213030,Taiteiden ja kulttuurin tutkimuksen laitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213020,Viestintätieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,214010,Bio- ja ympäristötieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,214020,Fysiikan laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,213050,Historian ja etnologian laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,213000,Humanistis-yhteiskuntatieteellinen tiedekunta,, Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,217000,Informaatioteknologian tiedekunta,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,217000,217010,Tietojenkäsittelytieteiden laitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,217000,217020,Tietotekniikan laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219700,Johto ja yliopiston yhteiset,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,211000,Kasvatustieteiden ja psykologian tiedekunta,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,211020,Kasvatustieteiden laitos,, Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,216000,Kauppakorkeakoulu,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,216000,216010,Avance Johtamiskoulutus,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,211000,Kasvatustieteiden tiedekunta,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,211000,211010,Opettajankoulutuslaitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,211000,211020,Kasvatustieteiden laitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,215000,Liikuntatieteellinen tiedekunta,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,214030,Kemian laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,213040,Kieli- ja viestintätieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213040,Kielten laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219120,Kokkolan yliopistokeskus Chydenius,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,20019140,Koulutuksen tutkimuslaitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219140,Koulutuksen tutkimuslaitos,, Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,215000,215020,Liikuntabiologian laitos,, Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,215000,215030,Liikuntakasvatuksen laitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,215000,215010,Terveystieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,215000,Liikuntatieteellinen tiedekunta,, Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,214000,Matemaattis-luonnontieteellinen tiedekunta,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,214000,214020,Fysiikan laitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,214000,214030,Kemian laitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,214000,214010,Bio- ja ympäristötieteiden laitos,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,214000,214040,Matematiikan ja tilastotieteen laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,214040,Matematiikan ja tilastotieteen laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219220,Monikielisen akateemisen viestinnän keskus,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213010,Musiikin laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,213030,"Musiikin, taiteen ja kulttuurin tutkimuksen laitos",, Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,210100,Normaalikoulu,, -Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,212000,Yhteiskuntatieteellinen tiedekunta,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,211010,Opettajankoulutuslaitos,, Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,212000,212020,Psykologian laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,211040,Psykologian laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,213060,Soveltavan kielentutkimuksen keskus,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213030,Taiteiden ja kulttuurin tutkimuksen laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,215000,215010,Terveystieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219230,Tiedemuseo,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,217000,217010,Tietojenkäsittelytieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,217000,217020,Tietotekniikan laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,213000,213020,Viestintätieteiden laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,212000,Yhteiskuntatieteellinen tiedekunta,, Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,212000,212010,Yhteiskuntatieteiden ja filosofian laitos,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,,,,http://isni.org/isni/0000000459488864,971 -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,10000,10000,Talous ja kulttuuri yhteiset,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,10000,11000,"Liiketalouden koulutusyksikkö, Kouvola",, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,10000,12000,"Liiketalouden koulutusyksikkö, Mikkeli",, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,10000,13000,Kulttuurin koulutusyksikkö,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,20000,Tekniikka yhteiset,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,21000,Rakennus- ja energiatekniikan koulutusyksikkö,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,22000,Informaatioteknologian koulutusyksikkö,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,23000,"Sähkö-, talo- ja materiaalitekniikan koulutusyksikkö",, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,24000,Metsätalouden ja ympäristöteknologian koulutusyksikkö,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,25000,Logistiikan ja merenkulun koulutusyksikkö,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,30000,Hyvinvointi yhteiset,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,31000,Kuntoutus- ja terveysalan koulutusyksikkö,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,32000,Terveysalan ja ensihoidon koulutusyksikkö,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,33000,Sosiaali- ja terveysalan koulutusyksikkö,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,34000,Sosiaalialan ja toimintakyvyn edistämisen koulutusyksikkö,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,35000,"Matkailu-,ravitsemis- ja nuorisoalan koulutusyksikkö",, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,,40000,Opetuksen hallinto,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,,51000,Opetuksen palvelut,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,,70000,"Talous, henkilöstö- ja hallintopalvelut",, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,60000,TKI ja palvelut,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,61000,TKI-palvelut,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62100,Digitaalinen talous,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62200,"Metsä, ympäristö ja energia",, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62300,Logistiikka ja merenkulku,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62400,Kestävä hyvinvointi,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62500,Luovat alat,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66000,Kuitulaboratorio,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66600,Kymilabs,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,213070,Yhteiskuntatieteiden ja filosofian laitos,, +Jyväskylän yliopisto,University of Jyväskylä,Jyväskylä universitet,01906,,219500,Yliopistopalvelut,, +KAUTE-säätiö,,,02014465,,,,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,,10118,,,,, Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66100,3K-tehdas,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,66400,Active Life Lab,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,66700,Biosampo,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,62100,Digitaalinen talous,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,66100,Elektroniikan 3K-tehdas,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,30000,Hyvinvointi yhteiset,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,22000,Informaatioteknologian koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,62400,Kestävä hyvinvointi,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,66000,Kuitulaboratorio,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,13000,Kulttuurin koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,31000,Kuntoutus- ja terveysalan koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,66600,Kymilabs,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,11000,"Liiketalouden koulutusyksikkö, Kouvola",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,12000,"Liiketalouden koulutusyksikkö, Mikkeli",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,25000,Logistiikan ja merenkulun koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,62300,Logistiikka ja merenkulku,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,62500,Luovat alat,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,66800,Luovien alojen tutkimusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,35000,"Matkailu, ravitsemis- ja nuorisoalan koulutusyksik",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,30000,35000,"Matkailu-,ravitsemis- ja nuorisoalan koulutusyksikkö",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,62200,"Metsä, ympäristö ja energia",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,24000,Metsätalouden ja ympäristöteknologian koulutusy,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,66300,Mikpolis,, Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66200,Nuorisoalan tutkimus- ja kehittämiskeskus Juvenia,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66300,Mikpolis,, -Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,60000,66900,Pienyrityskeskus,, -Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,,,http://isni.org/isni/0000000404149419,993 -Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,10,Liiketoiminta ja innovaatiot,, -Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,11,Kone- ja kaivostekniikka,, -Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,12,Tietojärjestelmät,, -Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,13,Sairaan- ja terveydenhoito,, -Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,14,Aktiviteettimatkailu,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,66200,Nuorisoalan tutkimus- ja kehittämisyksikkö Juven,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,40000,Opetuksen hallinto,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,51000,Opetuksen palvelut,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,66900,Pienyrityskeskus,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,21000,Rakennus- ja energiatekniikan koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,21000,Rakennus- ja energiatekniikan koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,33000,Sosiaali- ja terveysalan Mikkelin koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,36000,Sosiaali- ja terveysalan Savonlinnan koulutusyksik,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,34000,Sosiaalialan ja toimintakyvyn edistämisen koulutu,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,20000,23000,"Sähkö-, talo- ja materiaalitekniikan koulutusyksikkö",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,23000,"Sähkö-, talo- materiaalitekniikan koulutusyksikk",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,60000,TKI ja palvelut,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,61000,TKI-palvelut,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences (Xamk),,10118,10000,10000,Talous ja kulttuuri yhteiset,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,70000,"Talous, henkilöstö- ja hallintopalvelut",, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,10000,Talous- ja kulttuuri yhteiset,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,20000,Tekniikka yhteiset,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,32000,Terveysalan ja ensihoidon koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,42000,Yhteisten opintojen koulutusyksikkö,, +Kaakkois-Suomen ammattikorkeakoulu,South-Eastern Finland University of Applied Sciences,Kaakkois-Suomen ammattikorkeakoulu,10118,,14000,Yrittäjyys ja innovaatiot -yksikkö,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,,,, Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,15,Aikuis- ja täydennyskoulutuspalvelut AIKOPA,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,Kajaanin ammattikorkeakoulu,02473,,12,Business-osaamisalue,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,Kajaanin ammattikorkeakoulu,02473,,14,Kampuspalvelut,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,18,Kampuspalvelut,, Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,16,Koulutuspalvelut,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,Kajaanin ammattikorkeakoulu,02473,,13,Opiskelija- ja viestintäpalvelut,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,Kajaanin ammattikorkeakoulu,02473,,11,Sote-osaamisalue,, Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,17,TKI-palvelut,, -Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,,02473,,18,Kampuspalvelut,, -Karelia,Karelia University of Applied Sciences,,02469,,,,http://isni.org/isni/0000000406476472,1013 -Karelia,Karelia University of Applied Sciences,,02469,,1054,1054 Rehtorin toimisto,, -Karelia,Karelia University of Applied Sciences,,02469,,1055,1055 Opintoasiainpalvelut,, -Karelia,Karelia University of Applied Sciences,,02469,,1057,1057 Laskentapalvelut,, -Karelia,Karelia University of Applied Sciences,,02469,,1060,1060 Henkilöstöpalvelut,, -Karelia,Karelia University of Applied Sciences,,02469,,1063,1063 Tiedotus- ja markkinointi,, -Karelia,Karelia University of Applied Sciences,,02469,,1066,1066 Tietohallinto,, -Karelia,Karelia University of Applied Sciences,,02469,,1069,1069 Kirjasto,, -Karelia,Karelia University of Applied Sciences,,02469,,1072,1072 Kansainvälistymispalvelut,, -Karelia,Karelia University of Applied Sciences,,02469,,1970,1970 Projektitoiminta/YPA,, -Karelia,Karelia University of Applied Sciences,,02469,,2051,2051 TKI- ja palveluliiketoiminta,, -Karelia,Karelia University of Applied Sciences,,02469,,3051,3051 Metsätalous,, -Karelia,Karelia University of Applied Sciences,,02469,,3054,3054 Maaseutuelinkeinot,, -Karelia,Karelia University of Applied Sciences,,02469,,3101,3101 Energia- ja ympäristötekniikka,, +Kajaanin ammattikorkeakoulu,Kajaani University of Applied Sciences,Kajaanin ammattikorkeakoulu,02473,,10,Teknologia-osaamisalue,, Karelia,Karelia University of Applied Sciences,,02469,,5000,5000 Koulutuksen hallinto/Wärtsilä,, -Karelia,Karelia University of Applied Sciences,,02469,,5011,5011 TKI/Projektit/Tekniikan alat,, -Karelia,Karelia University of Applied Sciences,,02469,,5031,5031 Tietojenkäsittely,, -Karelia,Karelia University of Applied Sciences,,02469,,5051,5051 Konetekniikka,, -Karelia,Karelia University of Applied Sciences,,02469,,5054,5054 Rakennustekniikka,, -Karelia,Karelia University of Applied Sciences,,02469,,5057,5057 Tieto- ja viestintätekniikka,, -Karelia,Karelia University of Applied Sciences,,02469,,5061,5061 Talotekniikka,, -Karelia,Karelia University of Applied Sciences,,02469,,5064,5064 Sähkötekniikka,, -Karelia,Karelia University of Applied Sciences,,02469,,5067,5067 YAMK Teknologiaosaamisen johtaminen,, -Karelia,Karelia University of Applied Sciences,,02469,,5101,5101 Liiketalous,, -Karelia,Karelia University of Applied Sciences,,02469,,5104,5104 International business,, -Karelia,Karelia University of Applied Sciences,,02469,,5110,5110 YAMK Johtamisen ja liiketoimintaosaamisen koulutus,, -Karelia,Karelia University of Applied Sciences,,02469,,5999,5999 Elinkeinotoiminta/tekniikka,, -Karelia,Karelia University of Applied Sciences,,02469,,6051,6051 Medianomikoulutus,, -Karelia,Karelia University of Applied Sciences,,02469,,6054,6054 Musiikkipedagogikoulutus,, Karelia,Karelia University of Applied Sciences,,02469,,6057,6057 Muotoilu,, -Karelia,Karelia University of Applied Sciences,,02469,,6151,6151 Restonomi,, Karelia,Karelia University of Applied Sciences,,02469,,7000,7000 Koulutuksen hallinto/Tikkarinne,, -Karelia,Karelia University of Applied Sciences,,02469,,7051,7051 Sosionomi,, -Karelia,Karelia University of Applied Sciences,,02469,,7054,7054 Sairaanhoitaja,, -Karelia,Karelia University of Applied Sciences,,02469,,7057,7057 Fysioterapeutti,, -Karelia,Karelia University of Applied Sciences,,02469,,7061,7061 Terveydenhoitaja,, -Karelia,Karelia University of Applied Sciences,,02469,,7063,7063 YAMK Sosiaali- ja terveysalan kehittäminen ja johtaminen,, -Karelia,Karelia University of Applied Sciences,,02469,,7064,7064 YAMK Ikäosaaminen,, Karelia,Karelia University of Applied Sciences,,02469,,8067,8067 Maakuntakorkeakoulu,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,,02469,,,,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1054,1054 Rehtorin toimisto,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1055,1055 Opintoasiainpalvelut,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1053,1056 KV-palvelut,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1057,1057 Laskentapalvelut,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1060,1060 Henkilöstöpalvelut,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1063,1063 Tiedotus- ja markkinointi,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1066,1066 Tietohallinto,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1069,1069 Kirjasto,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1072,1072 Kansainvälistymispalvelut,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1080,1080 Opetuksen yhteiset henk.kulut,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,1970,1970 Projektitoiminta/YPA,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,2051,2051 TKI- ja palveluliiketoiminta,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,3051,3051 Metsätalous,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,3054,3054 Maaseutuelinkeinot,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,3101,3101 Energia- ja ympäristötekniikka,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5011,5011 TKI/Projektit/Tekniikan alat,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5031,5031 Tietojenkäsittely,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5051,5051 Konetekniikka,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5054,5054 Rakennustekniikka,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5057,5057 Tieto- ja viestintätekniikka,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5061,5061 Talotekniikka,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5064,5064 Sähkötekniikka,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5067,5067 YAMK Teknologiaosaamisen johtaminen,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5101,5101 Liiketalous,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5104,5104 International business,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5110,5110 YAMK Johtamisen ja liiketoimintaosaamisen kou,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,5999,5999 Elinkeinotoiminta/tekniikka,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,6051,6051 Medianomikoulutus,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,6054,6054 Musiikkipedagogikoulutus,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,6151,6151 Restonomi,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,7051,7051 Sosionomi,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,7054,7054 Sairaanhoitaja,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,7057,7057 Fysioterapeutti,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,7061,7061 Terveydenhoitaja,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,7063,7063 YAMK Sosiaali- ja terveysalan kehittäminen j,, +Karelia-ammattikorkeakoulu,Karelia University of Applied Sciences,Karelia-ammattikorkeakoulu,02469,,7064,7064 YAMK Ikäosaaminen,, +Koneen Säätiö,Kone Foundation,,02135371,,,,, +Kotimaisten kielten keskus,Institute for the Languages of Finland,,02458728,,,,, +Kuopion yliopistollisen sairaalan erityisvastuualue,Kuopio University Hospital Catchment Area,,01714953,,,,, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,,10126,,,,, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,ALAB,"Hallinto, LAB",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,HLTI,"Hyvinvointi, Lahti",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,HLPR,"Hyvinvointi, Lappeenranta",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,KLTI,"Kielikeskus, Lahti",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,KLPR,"Kielikeskus, Lappeenranta",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,LLTI,"Liiketalous, Lahti",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,LLPR,"Liiketalous, Lappeenranta",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,MLTI,"Marata, Lahti",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,MLPR,"Marata, Lappeenranta",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,MILTI,"Muotoiluinstituutti, Lahti",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,MILPR,"Muotoiluinstituutti, Lappeenranta",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,TLTI,"Teknologia, Lahti",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,TLPR,"Teknologia, Lappeenranta",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,CLTI,"Tietojenkäsittely, Lahti",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,CLPR,"Tietojenkäsittely, Lappeenranta",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,ILTI,"Tietotekniikka, Lahti",, +LAB-ammattikorkeakoulu,LAB University of Applied Sciences,LAB-ammattikorkeakoulu,10126,,ILPR,"Tietotekniikka, Lappeenranta",, Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,,,http://isni.org/isni/0000000404178973,1109 -Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,LI,Liiketalous ja matkailu,, Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,LP,LAMK Palvelut,, -Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,MD,Musiikki- ja draamainstituutti,, +Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,LI,Liiketalous ja matkailu,, Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,MI,Muotoiluinstituutti,, +Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,MD,Musiikki- ja draamainstituutti,, Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,ST,Sosiaali- ja terveys,, Lahden ammattikorkeakoulu,Lahti University of Applied Sciences,,02470,,TE,Tekniikka ja ympäristö,, -Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,,,http://isni.org/isni/0000000446491976,1133 +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,,,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,Lapin ammattikorkeakoulu,10108,,300,Arktiset luonnonvarat ja talous,, Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,120,Hyvinvointipalvelut,, Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,130,Kauppa ja kulttuuri,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,111,Korkeakoulupalvelut,, Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,140,Matkailupalvelut,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,Lapin ammattikorkeakoulu,10108,,200,Pohjoinen hyvinvointi ja palvelut,, Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,150,Teollisuus ja luonnonvarat,, Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,160,YAMK,, Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,110,Yhteiset toiminnot,, -Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,,10108,,111,Korkeakoulupalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,,,http://isni.org/isni/000000010744995X,3448 -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,221200,Harjoittelukoulu,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,222000,Oikeustieteiden tiedekunta,, +Lapin ammattikorkeakoulu,Lapland University of Applied Sciences,Lapin ammattikorkeakoulu,10108,,100,Yhteiset toiminnot,, +Lapin yliopisto,University of Lapland,,01918,,,,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2263400,AK kirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2250000,Arktinen hallinta ja hallinto,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,225000,Arktinen keskus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2250600,Arktisen antropologian tutkimusryhma,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250600,Arktisen antropologian tutkimusryhmä,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2250500,Arktisen globaalimuutoksen tutkimusryhma,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250500,Arktisen globaalimuutoksen tutkimusryhmä,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,200250400,Arktisen kestävän kehityksen tutkimusryhmä,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299050,Arktisen opetuksen ja tutkimuksen rahasto,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226100,Avoin yliopisto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2261100,Avoin yliopisto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,229000,Hallintoyksikko,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2291600,Hankintatoimi,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,221200,Harjoittelukoulu,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,221300,Harjoittelukoulu (ylakoulu),, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2212100,Harjoittelukoulu perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2212000,Harjoittelukoulun hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2291200,Henkilostoasiat,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2298400,Henkilostoasiat,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291200,Henkilöstöasiat,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298400,Henkilöstöasiat,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2291400,IT-palvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2210000,KTK hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2211100,KTK perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2264200,Kaannospalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2297100,Kansainvalistymispalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299010,Kasvatustieteiden opetuksen ja tutkimuksen rahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,221000,Kasvatustieteiden tiedekunta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2264000,Kielikeskuksen hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226400,Kielikeskus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2264100,Kielikeskus,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,20026300,200263000,Kirjaston hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2263000,Kirjaston hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2294100,Kongressipalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2264300,Konsernin kielikeskus,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,20026300,200263050,Konsernin kirjasto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,20026300,2263200,Taidekirjasto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,20026300,2263400,AK kirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2263050,Konsernin kirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226200,Koulutus- ja kehittamispalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2262100,Koulutus- ja kehittamispalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226200,2262100,Koulutus- ja kehittämispalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2298801,Kulttuurilahtoinen palvelumuotoilu -tohtoriohjelma,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298801,Kulttuurilähtöinen palvelumuotoilu -tohtoriohjelma,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226400,2264200,Käännöspalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,228000,LUC Tietohallinto ja IT-palvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2280100,LUC Tietohallinto ja IT-palvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2290800,Laadunhallintapalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2293300,Lapin korkeakoulukonserni,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226350,Lapin yliopistokustannus,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226400,Kielikeskus,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2293300,Lapin korkeakoulukonserni,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,229800,Yliopistotason tulot ja menot,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,221000,Kasvatustieteiden tiedekunta,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,223000,Taiteiden tiedekunta,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,221000,2210000,KTK hallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,221000,2211100,KTK perustoiminta,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,221000,2211200,Opetus- ja kasvatusalan koulutuspalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,221200,2212000,Harjoittelukoulun hallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,221200,2212100,Harjoittelukoulu perustoiminta,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,222000,2220000,OTK hallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,222000,2220100,OTK perustoiminta,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,223000,2230000,TTK hallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,223000,2230100,TTK perustoiminta,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,223000,2230110,TTK elinkeinotoiminta,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,223000,2230120,TTK täydentävä rahoitus,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,224000,Yhteiskuntatieteiden tiedekunta,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224000,2240000,YTK hallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224000,2240100,YTK perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2263500,Lapin yliopistokustannus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299100,Lapin yliopiston profilointirahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2296100,Lapin yliopistorahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2245000,MTI hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2245200,MTI taydentava rahoitus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224500,2245200,MTI täydentävä rahoitus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,224500,Matkailun tutkimus- ja koulutusinstituutti,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2245100,Matkailun tutkimus- ja koulutusinstituutti,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2220000,OTK hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2220100,OTK perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299020,Oikeustieteiden opetuksen ja tutkimuksen rahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,222000,Oikeustieteiden tiedekunta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2211200,Opetus- ja kasvatusalan koulutuspalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2292100,Opetusvararehtori,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2292200,Opiskelijapalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2263100,Paakirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226300,Paakirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2250400,Pohjoinen Poliittinen Talous,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,224100,Pohjoisen yhteiskunnan tutkimusinstituutti,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224100,2241100,Pohjoisen yhteiskunnan tutkimusinstituutti,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,224500,Matkailun tutkimus- ja koulutusinstituutti,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224500,2245000,MTI hallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224500,2245100,Matkailun tutkimus- ja koulutusinstituutti,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,224500,2245200,MTI täydentävä rahoitus,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250000,Arktinen hallinta ja hallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250100,Tiedekeskusnäyttely,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250200,Tiedeviestintä,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2241100,Pohjoisen yhteiskunnan tutkimusinstituutti (LAPPEA,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2250300,Pohjoisen ymparisto- ja vahemmistooikeuden institu,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250300,Pohjoisen ympäristö- ja vähemmistöoikeuden instituutti,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,200250400,Arktisen kestävän kehityksen tutkimusryhmä,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250500,Arktisen globaalimuutoksen tutkimusryhmä,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226100,2261100,Avoin yliopisto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,226200,Koulutus- ja kehittämispalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226200,2262100,Koulutus- ja kehittämispalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2298802,Pohjoiset kulttuurit ja kestava luonnonvarapolitii,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298802,Pohjoiset kulttuurit ja kestävä luonnonvarapolitiikka,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,20026300,Pääkirjasto,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,20026300,200263100,Pääkirjasto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226350,2263500,Lapin yliopistokustannus,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226400,2264000,Kielikeskuksen hallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226400,2264100,Kielikeskus,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226400,2264200,Käännöspalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,226400,2264300,Konsernin kielikeskus,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,229000,Hallintoyksikkö,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290100,Rehtori,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290200,Yleishallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290300,Suunnittelupalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290310,Taloushallintopalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290500,Viestintä- ja suhdetoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2290100,Rehtori,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2297300,Strateginen kansainvalistyminen,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2290300,Suunnittelupalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2230110,TTK elinkeinotoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2230000,TTK hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2230100,TTK perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2230120,TTK taydentava rahoitus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,223000,2230120,TTK täydentävä rahoitus,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2263200,Taidekirjasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299030,Taiteiden opetuksen ja tutkimuksen rahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,223000,Taiteiden tiedekunta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2290310,Taloushallintopalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2298310,Taloushallintopalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2290600,Taydentavan rahoituksen palvelupiste,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2290700,Tekniset erityispalvelut,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2250100,Tiedekeskusnayttely,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250100,Tiedekeskusnäyttely,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2293100,Tiedevararehtori,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2250200,Tiedeviestinta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,225000,2250200,Tiedeviestintä,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2298804,Tieteenfilosofia ja tutkijantaidot,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2291300,Tietohallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2291500,Tilahallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2295800,Tutkijakoulu,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2291800,Tutkimuspalvelut,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290600,Täydentävän rahoituksen palvelupiste,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290700,Tekniset erityispalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290800,Laadunhallintapalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291100,Vararehtori,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291200,Henkilöstöasiat,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291300,Tietohallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291400,IT-palvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291500,Tilahallinto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291600,Hankintatoimi,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291700,Yliopistopaino,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2291800,Tutkimuspalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2292100,Opetusvararehtori,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2292200,Opiskelijapalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2293100,Tiedevararehtori,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2294100,Kongressipalvelut,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2295100,University of Arctic -rahasto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2296100,Lapin yliopistorahasto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2297100,Kansainväliset asiat,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2297600,Uarctic perustoiminta,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2297500,Uarctic-verkosto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298400,Henkilöstöasiat,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298801,Kulttuurilähtöinen palvelumuotoilu -tohtoriohjelma,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298802,Pohjoiset kulttuurit ja kestävä luonnonvarapolitiikka,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2295100,University of Arctic -rahasto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2291100,Vararehtori,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2290500,Viestinta- ja suhdetoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229000,2290500,Viestintä- ja suhdetoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2240000,YTK hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2240100,YTK perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299040,Yhteiskuntatieteiden opetuksen ja tutkimuksen raha,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,224000,Yhteiskuntatieteiden tiedekunta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2298803,Yhteisot ja muuttuva tyo,, Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298803,Yhteisöt ja muuttuva työ,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,229800,2298804,Tieteenfilosofia ja tutkijantaidot,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299010,Kasvatustieteiden opetuksen ja tutkimuksen rahasto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299020,Oikeustieteiden opetuksen ja tutkimuksen rahasto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299030,Taiteiden opetuksen ja tutkimuksen rahasto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299040,Yhteiskuntatieteiden opetuksen ja tutkimuksen rahasto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299050,Arktisen opetuksen ja tutkimuksen rahasto,, -Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2299100,Lapin yliopiston profilointirahasto,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,,,http://isni.org/isni/0000000105333048,1150 -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A100A,PÄÄT Tiedekunnan yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A110A,PÄÄT Kanslia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A120A,PÄÄT Tiedekunnan opintopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A130A,PÄÄT KTK-yleisopinnot,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A200A,PÄÄT Talouden ja yritysjuridiikan laitos,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A210A,PÄÄT Laskentatoimi,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A220A,PÄÄT Rahoitus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A230A,PÄÄT Strategiatutkimus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A240A,PÄÄT Yritysjuridiikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A250A,PÄÄT Talousjohtaminen (KTK),, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A300A,PÄÄT Johtamisen ja kv-liiketoiminnan lai,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A310A,PÄÄT Hankintojen johtaminen,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A320A,PÄÄT Johtaminen ja organisaatiot,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A330A,PÄÄT Kansainvälinen markkinointi,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A340A,PÄÄT Tietojohtaminen,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A350A,PÄÄT MSIS,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A360E,PÄÄTLahden yksikkö,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A365A,PÄÄT Tietojohtaminen ja johtajuus (KTM),, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A370A,PÄÄT Johtaminen (KTK),, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A380A,PÄÄT Kv-liiketoiminta (KTK),, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A390A,PÄÄT Strateginen johtaminen (KTM),, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A410A,PÄÄT Nordi / KATI,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A500A,PÄÄT TBRC/KATI,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B100A,Teknillisen tiedekunnan yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B101A,PÄÄT Hybridilaboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B102A,"Yk-lisät,Tekn.tdk yhteiset",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B110A,Toimistopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B111A,PÄÄT Toimistopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B120A,Opintopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B130A,PÄÄT Tekniset palvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B131A,PÄÄT Elektroniikan suunnittelukeskus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B132A,PÄÄT Mekaniikkapaja,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B133A,PÄÄT TEPA Energia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B134A,PÄÄT TEPA Kemia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B135A,PÄÄT TEPA Metalli,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2213000,Ylakoulun hallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2213100,Ylakoulun perustoiminta,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2290200,Yleishallinto,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,2291700,Yliopistopaino,, +Lapin yliopisto,University of Lapland,Lapplands universitet,01918,,229800,Yliopistotason tulot ja menot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K462A,AV-palvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B302A,Analyysipalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K810A,"Apurahajärjestelmä, LUT",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F150A,Asiakkuudet,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F130A,Avoin yliopisto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB22A,Back Office,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B390A,CEID-keskus yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E183A,Cost Management,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B363A,Erotuskonseptit,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E170A,Etelä-Karjala-instituutti,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB21A,Front Office,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K500A,HR,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K110A,Hakupalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K471A,Hankintapalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E180A,IEM Yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K400A,IS&T,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB20A,IS&T + Project office,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F220A,Imatran toimipiste,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB53A,Imatran toimipiste,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC30A,Innovaatiopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC10A,"Innovaatiot ja yrittäjyys, yhteiset",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC11A,"Innovaatiot ja yrittäjyys, yk-laskenta",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E182A,Innovation Management,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E130A,Innovation and software,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E160D,Innovation and software/KOUVOLA,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E151E,Innovation and software/LAHTI,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E150E,"Internat business,marketing,entrep/LAHTI",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E120A,"International business,marketing,entrepr",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B380A,Inversio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K800A,Kansainvälisyys,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B365A,Kemiallinen metrologia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B351A,Kemialliset erotustekniikat,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B140A,Kielikeskus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B141A,PÄÄT Kielikeskus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B150A,PÄÄT CST,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B170A,PÄÄT LUT voima,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B200A,LUT School of Energy Systems yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B201A,"Yk-lisät, LES",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B210A,Energiatekniikan laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B211A,Lämpö- ja virtaustekniikan laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B212A,Teknillinen termodynamiikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B213A,Virtaustekniikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B214A,Uusiutuvien energiajärjestelmien laborat,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B215A,PÄÄT Ydinvoimatekniikan laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B216A,PÄÄT Ydinturvallisuuden tutkimusyksikkö,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B217A,Ydintekniikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B220G,Bioenergian laboratorio/Mikkeli,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B221J,PÄÄT Energia/Varkaus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B230A,Sähkötekniikan laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B231A,Sähkömarkkinalaboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B232A,Sähkönkäyttötekniikan laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B233A,CDMC-laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B234A,Säätö- ja digitaalitekniikan laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B235A,Sovelletun elektroniikan laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B236A,PÄÄT Sähkömekaaniset järjestelmät,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B237A,Aurinkotalousprofessuuri,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B240A,"Kestävyystutkimus, Lappeenranta",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B241E,"Kestävyystutkimus, Lahti",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B241G,PÄÄT Vihreä kemia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B250A,LUT Kone yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B250G,PÄÄT Materiaalitekniikka/Mikkeli,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B251A,Hitsaustekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B354A,Kiinteä/neste-erotus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B382A,Kiinteän aineen fysiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K210A,Kirjanpito,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K620A,Kirjanpito,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F200A,Kirjasto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F100A,Koke yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B386A,Konenäkö ja hahmontunnistus,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B252N,Koneteknologia/Turku,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B253A,Lasertyöstö,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B254A,Tuotantotekniikan laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B255A,Koneensuunnittelun laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B256A,Teräsrakenteiden laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E163D,Kouvola,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E162D,Kouvola/common,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B257C,Kuitukomposiittilaboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B258A,Konedynamiikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B259A,Älykkäiden koneiden laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B260G,PÄÄT LUT Savo,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B290A,LUT Voima,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2AAA,Alusta LES Reflex,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2ABA,Alusta LES SIM,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2ACA,Alusta LES RE-SOURSE,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2ADA,Alusta LES DIGI-USER,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2AEA,Alusta LES SAWE,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B2AFA,Alusta LES RED,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K330A,Kumppanuudet ja Venäjätoiminnot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K470A,Käyttötukipalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J310A,LUMA,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B300A,LUT Kemia yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B301A,CST,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B302A,Analyysipalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B303A,LENS tekniset palvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KA00A,LUT tohtorikoulu,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB51A,LUTin kirjastopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E153E,Lahti/common,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J130A,Lakiasiat,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B381A,Laskennallinen materiaalitiede,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB40A,Lähipalvelut,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B304A,Matematiikan ja fysiikan laitoksen yht,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B305A,"Opetus, laskennallinen tekniikka",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B306A,"Opetus, kemian tekniikka",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B308A,"Yk-lisät, LENS",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B309A,School of Engineering Science yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B310A,PÄÄT Kemia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B311A,PÄÄT Teknillinen kemia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B312A,PÄÄT Membraanitekniikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B320A,PÄÄT Yksikköoperaatiot,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B321A,PÄÄT Tuote- ja prosessikehitys,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B322A,PÄÄT Systeemitekniikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B323A,PÄÄT Fysikaalinen kemia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B324A,PÄÄT Kiinteä/neste-erotus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B325A,PÄÄT Virtausprosessi,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B326A,PÄÄT Erotustekniikka ja prosessi-integro,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B330A,PÄÄT Kuitu- ja paperitekniikan laborator,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B331A,PÄÄT Paperinjalostus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B340H,PÄÄT Fiber Tech -tutkimusyksikkö/Savonli,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B351A,Kemialliset erotustekniikat,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B383H,Materiaalifysiikka,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B352A,Membraanitekniikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B353A,Termiset yksikköoperaatiot,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B354A,Kiinteä/neste-erotus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K472A,Mikroluokkapalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K301A,Nordi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E161D,Operations management and system/KOUVOLA,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E140A,Operations management and systems engine,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E152E,Operations management and systems/LAHTI,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B306A,"Opetus, kemian tekniikka",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B305A,"Opetus, laskennallinen tekniikka",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K480A,Opetusteknologia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K120A,Opintoasioiden kehittäminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B120A,Opintopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E101A,Opintopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K100A,Opintotoimisto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B385A,Optoelektroniikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K450A,Palvelukehitys,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K460A,Perusresurssipalvelut,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B361A,Prosessikehitys,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B362A,Virtausprosessit,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B363A,Erotuskonseptit,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K473A,Puhelinpalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K440A,PÄÄT AV-palvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C500A,PÄÄT Arvoverkostojen johtaminen,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B364A,PÄÄT Biomateriaalit,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B365A,Kemiallinen metrologia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B371G,Vihreä kemia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B380A,Inversio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B381A,Laskennallinen materiaalitiede,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B382A,Kiinteän aineen fysiikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B383H,Materiaalifysiikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B385A,Optoelektroniikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B386A,Konenäkö ja hahmontunnistus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B387D,PÄÄT Konenäkö ja hahmotunnistus Kouvola,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B388A,PÄÄT Kemiallinen metrologia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B390A,CEID-keskus yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B391J,Varkaus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3AAA,Alusta LENS Reflex,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3ABA,Alusta LENS SIM,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3ACA,Alusta LENS RE-SOURSE,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3ADA,Alusta LENS DIGI-USER,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3AEA,Alusta LENS SAWE,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B3AFA,Alusta LENS RED,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B400A,PÄÄT LUT Kone yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B600A,PÄÄT CEID-keskus yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B150A,PÄÄT CST,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K490A,PÄÄT Digipaino,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B131A,PÄÄT Elektroniikan suunnittelukeskus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B221J,PÄÄT Energia/Varkaus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B326A,PÄÄT Erotustekniikka ja prosessi-integro,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F300A,PÄÄT Etelä-Karjala-instituutti,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B340H,PÄÄT Fiber Tech -tutkimusyksikkö/Savonli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B323A,PÄÄT Fysikaalinen kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A310A,PÄÄT Hankintojen johtaminen,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B410A,PÄÄT Hitsaustekniikan laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B411N,PÄÄT Koneteknologia/Turku,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B412A,PÄÄT Lasertyöstö,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B413G,PÄÄT Materiaalitekniikka/Mikkeli,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B420A,PÄÄT Konepajan ja levytyötekniikan labor,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B101A,PÄÄT Hybridilaboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C272A,PÄÄT Innovaatio- ja teknolog./TBRC,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C236A,PÄÄT Innovaatio- ja teknologiajohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C270A,PÄÄT Innovaatio- ja teknologiajohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C271A,PÄÄT Innovaatio- ja teknologiajohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C400A,PÄÄT Innovaatiojohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B510A,PÄÄT Inversio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A370A,PÄÄT Johtaminen (KTK),, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A320A,PÄÄT Johtaminen ja organisaatiot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A300A,PÄÄT Johtamisen ja kv-liiketoiminnan lai,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A130A,PÄÄT KTK-yleisopinnot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A330A,PÄÄT Kansainvälinen markkinointi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C211A,PÄÄT Kansainväliset toiminnot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A110A,PÄÄT Kanslia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B310A,PÄÄT Kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B388A,PÄÄT Kemiallinen metrologia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B141A,PÄÄT Kielikeskus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B324A,PÄÄT Kiinteä/neste-erotus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B460A,PÄÄT Konedynamiikka,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B430A,PÄÄT Koneensuunnittelun laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B440A,PÄÄT Teräsrakenteiden laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B531D,PÄÄT Konenäkö ja hahmontunnistus Kouvola,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B530A,PÄÄT Konenäkö ja hahmotunnistus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B387D,PÄÄT Konenäkö ja hahmotunnistus Kouvola,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C320A,PÄÄT Konenäön ja hahmontunnistuksen labo,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B420A,PÄÄT Konepajan ja levytyötekniikan labor,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B411N,PÄÄT Koneteknologia/Turku,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C600D,PÄÄT Kouvolan yksikkö,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B330A,PÄÄT Kuitu- ja paperitekniikan laborator,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B450C,PÄÄT Kuitukomposiittilaboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B460A,PÄÄT Konedynamiikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B470A,PÄÄT Älykkäiden koneiden laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B500A,PÄÄT Matematiikan ja fysiikan laitokset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B505A,PÄÄT Mafy yhteiset opetus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B510A,PÄÄT Inversio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B511G,PÄÄT Materiaalitekniikka/Mikkeli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C233A,PÄÄT Kustannusjohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A380A,PÄÄT Kv-liiketoiminta (KTK),, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K410A,PÄÄT Käyttötuki,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B400A,PÄÄT LUT Kone yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B260G,PÄÄT LUT Savo,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B170A,PÄÄT LUT voima,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C700E,PÄÄT Lahti School of Innovation,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C250E,PÄÄT Lahti/School of Innovation,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B412A,PÄÄT Lasertyöstö,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B515A,PÄÄT Laskennallinen materiaalitiede,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A210A,PÄÄT Laskentatoimi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A350A,PÄÄT MSIS,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B505A,PÄÄT Mafy yhteiset opetus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B500A,PÄÄT Matematiikan ja fysiikan laitokset,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B520A,PÄÄT Materiaalifysiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B250G,PÄÄT Materiaalitekniikka/Mikkeli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B413G,PÄÄT Materiaalitekniikka/Mikkeli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B511G,PÄÄT Materiaalitekniikka/Mikkeli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B132A,PÄÄT Mekaniikkapaja,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B312A,PÄÄT Membraanitekniikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F400A,PÄÄT Nordi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A410A,PÄÄT Nordi / KATI,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C330A,PÄÄT Ohjelmistotekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C300A,PÄÄT Ohjelmistotuotanto ja tiedonhallint,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C800A,PÄÄT Ohjelmistotutanto ja tiedonhallinta,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B525A,PÄÄT Optoelektroniikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B530A,PÄÄT Konenäkö ja hahmotunnistus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B531D,PÄÄT Konenäkö ja hahmontunnistus Kouvola,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B600A,PÄÄT CEID-keskus yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B610A,PÄÄT Älykkäiden koneiden laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B620J,PÄÄT Varkaus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C100A,PÄÄT Tiedekunnan hallintopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C110A,PÄÄT Tiedekunnan opintopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C120A,PÄÄT Tiedekunnan yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C200A,PÄÄT Tuotantotalouden osaston yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C210A,PÄÄT Teollisen markkinoinnin ja kansainv,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C211A,PÄÄT Kansainväliset toiminnot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K430A,PÄÄT Palvelutuotanto,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B331A,PÄÄT Paperinjalostus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K420A,PÄÄT Project Office,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C232A,PÄÄT Päätöksenteontukisysteemit,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A220A,PÄÄT Rahoitus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C262D,PÄÄT Rautatielogistiikka/Kouvola,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C240A,PÄÄT Ryhmäpäätöksenteon ja teknologian j,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J120A,PÄÄT Strategia ja yhteiskuntasuhteet,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A230A,PÄÄT Strategiatutkimus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A390A,PÄÄT Strateginen johtaminen (KTM),, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B322A,PÄÄT Systeemitekniikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B236A,PÄÄT Sähkömekaaniset järjestelmät,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23G300A,PÄÄT TBRC,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A500A,PÄÄT TBRC/KATI,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B133A,PÄÄT TEPA Energia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B134A,PÄÄT TEPA Kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B135A,PÄÄT TEPA Metalli,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A200A,PÄÄT Talouden ja yritysjuridiikan laitos,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A250A,PÄÄT Talousjohtaminen (KTK),, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B311A,PÄÄT Teknillinen kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B130A,PÄÄT Tekniset palvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C234A,PÄÄT Teknologiayrittäjyys,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C212A,PÄÄT Teollinen markkinointi,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C220A,PÄÄT Toimitusketjun johtamisen laborator,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C221A,PÄÄT Toimitusketjun johtaminen/TBRC,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C210A,PÄÄT Teollisen markkinoinnin ja kansainv,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C230A,PÄÄT Teollisuustalouden laboratorio yhte,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C231A,PÄÄT Teollisuustalous,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C232A,PÄÄT Päätöksenteontukisysteemit,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C233A,PÄÄT Kustannusjohtaminen,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C234A,PÄÄT Teknologiayrittäjyys,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C235A,PÄÄT Tietojohtaminen/tuotantotalous,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C236A,PÄÄT Innovaatio- ja teknologiajohtaminen,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C240A,PÄÄT Ryhmäpäätöksenteon ja teknologian j,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C241M,PÄÄT Tuotantotalous/Lapua,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C250E,PÄÄT Lahti/School of Innovation,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C260D,PÄÄT Tuotantotalous/Kouvola yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C261D,PÄÄT Tuotantotalous/Kouvola,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C262D,PÄÄT Rautatielogistiikka/Kouvola,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C270A,PÄÄT Innovaatio- ja teknologiajohtaminen,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C271A,PÄÄT Innovaatio- ja teknologiajohtaminen,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C272A,PÄÄT Innovaatio- ja teknolog./TBRC,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B440A,PÄÄT Teräsrakenteiden laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C100A,PÄÄT Tiedekunnan hallintopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A120A,PÄÄT Tiedekunnan opintopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C110A,PÄÄT Tiedekunnan opintopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A100A,PÄÄT Tiedekunnan yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C120A,PÄÄT Tiedekunnan yhteiset,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C281A,PÄÄT Tieto- ja kustannusjohtaminen,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C283A,PÄÄT Tieto- ja kustannusjohtaminen/TBRC,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C300A,PÄÄT Ohjelmistotuotanto ja tiedonhallint,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A340A,PÄÄT Tietojohtaminen,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A365A,PÄÄT Tietojohtaminen ja johtajuus (KTM),, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C235A,PÄÄT Tietojohtaminen/tuotantotalous,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C340A,PÄÄT Tietojohtaminen/tuta,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C310A,PÄÄT Tietoliikenneohjelmistojen laborato,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C320A,PÄÄT Konenäön ja hahmontunnistuksen labo,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C321D,PÄÄT Tietoyhteiskunnan teknologiat/Kouvo,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C330A,PÄÄT Ohjelmistotekniikan laboratorio,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C340A,PÄÄT Tietojohtaminen/tuta,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C400A,PÄÄT Innovaatiojohtaminen,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C500A,PÄÄT Arvoverkostojen johtaminen,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C600D,PÄÄT Kouvolan yksikkö,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C700E,PÄÄT Lahti School of Innovation,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C800A,PÄÄT Ohjelmistotutanto ja tiedonhallinta,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E100A,Kanslia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E101A,Opintopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E102A,Schoolin yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E103A,"Yk-lisät, LBM",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E110A,"Strategy,management and accounting",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E120A,"International business,marketing,entrepr",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E121G,IBME/Mikkeli,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E130A,Innovation and software,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E140A,Operations management and systems engine,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E150E,"Internat business,marketing,entrep/LAHTI",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E151E,Innovation and software/LAHTI,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E152E,Operations management and systems/LAHTI,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E153E,Lahti/common,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E160D,Innovation and software/KOUVOLA,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E161D,Operations management and system/KOUVOLA,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E162D,Kouvola/common,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E163D,Kouvola,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E170A,Etelä-Karjala-instituutti,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E180A,IEM Yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E181A,Software Engineering,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E182A,Innovation Management,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E183A,Cost Management,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E184A,Systems Engineering,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E185A,Supply Chain Management,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1AAA,Alusta LBM Reflex,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1ABA,Alusta LBM SIM,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1ACA,Alusta LBM RE-SOURSE,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1ADA,Alusta LBM DIGI-USER,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1AEA,Alusta LBM SAWE,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E1AFA,Alusta LBM RED,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F100A,Koke yhteiset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F101A,"Yk-lisät, Koulutus- ja kehittämiskeskus",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F110A,Täydennyskoulutus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F120A,Tutkintotavoitteinen koulutus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F130A,Avoin yliopisto,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F140A,Tutkimus- ja kehittämistoiminta,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F150A,Asiakkuudet,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F200A,Kirjasto,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F201A,"Yk-lisät, Kirjasto",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F210A,Yhteinen tiedekirjasto,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F220A,Imatran toimipiste,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B111A,PÄÄT Toimistopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C221A,PÄÄT Toimitusketjun johtaminen/TBRC,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C220A,PÄÄT Toimitusketjun johtamisen laborator,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C200A,PÄÄT Tuotantotalouden osaston yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C261D,PÄÄT Tuotantotalous/Kouvola,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C260D,PÄÄT Tuotantotalous/Kouvola yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23C241M,PÄÄT Tuotantotalous/Lapua,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B321A,PÄÄT Tuote- ja prosessikehitys,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B620J,PÄÄT Varkaus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B241G,PÄÄT Vihreä kemia,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B325A,PÄÄT Virtausprosessi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B216A,PÄÄT Ydinturvallisuuden tutkimusyksikkö,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B215A,PÄÄT Ydinvoimatekniikan laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B320A,PÄÄT Yksikköoperaatiot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A240A,PÄÄT Yritysjuridiikka,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B470A,PÄÄT Älykkäiden koneiden laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B610A,PÄÄT Älykkäiden koneiden laboratorio,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23A360E,PÄÄTLahden yksikkö,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K640A,Rahoitus ja tarkastustoiminta,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F230A,Saimia Kirjasto,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F300A,PÄÄT Etelä-Karjala-instituutti,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F400A,PÄÄT Nordi,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23G300A,PÄÄT TBRC,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J100A,Yliopiston johto,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J110A,Toiminta,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J120A,PÄÄT Strategia ja yhteiskuntasuhteet,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J130A,Lakiasiat,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J200A,Toiminnan ohjaus ja laadunhallinta,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J210A,Toiminnan suunnittelu,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J300A,Erityishankkeet,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J310A,LUMA,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J400A,LUT Investointiohjelma,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J410A,IPR ja patentit,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J500A,Alusta REFLEX,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J501A,Alusta SIM,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J502A,Aloittavat tutkimusalustat,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J503A,Alusta RE-SOURSE,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J504A,Alusta DIGI-USER,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J505A,Alusta SAWE,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J506A,Alusta RED,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K100A,Opintotoimisto,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K110A,Hakupalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K120A,Opintoasioiden kehittäminen,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K200A,Talouspalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K210A,Kirjanpito,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB52A,Saimian kirjastopalvelut,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K220A,Sisäinen laskenta,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K300A,Tutkimus- ja innovaatiopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K301A,Nordi,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K630A,Sisäinen laskenta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E181A,Software Engineering,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E110A,"Strategy,management and accounting",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E185A,Supply Chain Management,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23E184A,Systems Engineering,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K302A,TBRC,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K310A,Kansallinen rahoitus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K320A,EU-rahoitus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K330A,Kumppanuudet ja Venäjätoiminnot,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K340A,Tutkimuspolitiikka,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K400A,IS&T,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K410A,PÄÄT Käyttötuki,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K420A,PÄÄT Project Office,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K430A,PÄÄT Palvelutuotanto,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K440A,PÄÄT AV-palvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K450A,Palvelukehitys,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K460A,Perusresurssipalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K461A,Tulostus- ja kopiointipalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K462A,AV-palvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K470A,Käyttötukipalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K471A,Hankintapalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K472A,Mikroluokkapalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K473A,Puhelinpalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K480A,Opetusteknologia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K490A,PÄÄT Digipaino,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K500A,HR,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K600A,Tilapalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K200A,Talouspalvelut,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K610A,Talouspalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K620A,Kirjanpito,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K630A,Sisäinen laskenta,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K640A,Rahoitus ja tarkastustoiminta,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K680A,Tilapalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K690A,Tilakustannukset,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K700A,Viestintäpalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K800A,Kansainvälisyys,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K810A,"Apurahajärjestelmä, LUT",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K820A,Opiskelijapalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K830A,Koulutuspalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K840A,Tietojärjestelmien kehittäminen,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K850A,Apurahat,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K860A,Kielikeskus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K870A,Opetusteknologia,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K900A,Yliopistopaino,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KA00A,LUT tohtorikoulu,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B100A,Teknillisen tiedekunnan yhteiset,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B353A,Termiset yksikköoperaatiot,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K600A,Tilapalvelut,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB10A,Toiminnan ohjaus ja laadun hallinta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23J200A,Toiminnan ohjaus ja laadunhallinta,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB11A,Toiminnan suunnittelu,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB20A,IS&T + Project office,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB21A,Front Office,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB22A,Back Office,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB23A,Yliopistopaino,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB40A,Lähipalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B110A,Toimistopalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K461A,Tulostus- ja kopiointipalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F140A,Tutkimus- ja kehittämistoiminta,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F120A,Tutkintotavoitteinen koulutus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F110A,Täydennyskoulutus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC40A,Täydennyskoulutus,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC50A,Urapalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K700A,Viestintäpalvelut,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B362A,Virtausprosessit,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F210A,Yhteinen tiedekirjasto,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB50A,Yhteiskirjasto,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB51A,LUTin kirjastopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB52A,Saimian kirjastopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB53A,Imatran toimipiste,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F201A,"Yk-lisät, Kirjasto",, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB54A,"Yk-lisät, Kirjasto",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC10A,"Innovaatiot ja yrittäjyys, yhteiset",, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC11A,"Innovaatiot ja yrittäjyys, yk-laskenta",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23F101A,"Yk-lisät, Koulutus- ja kehittämiskeskus",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23B102A,"Yk-lisät,Tekn.tdk yhteiset",, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23K900A,Yliopistopaino,, +Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KB23A,Yliopistopaino,, Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC20A,Yrittäjyys,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC30A,Innovaatiopalvelut,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC40A,Täydennyskoulutus,, -Lappeenrannan teknillinen yliopisto,Lappeenranta University of Technology,,01914,,23KC50A,Urapalvelut,, -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,,,http://isni.org/isni/0000000404001203,1179 -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,HL,Liiketalous,, -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,HS,Turvallisuusala,, -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,LAU,Laurea,, -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,MP,Matkailu- ja palveluliiketoiminta,, -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,NT,Tietojenkäsittely,, -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,SF,Fysioterapia,, -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,SH,Terveysala,, -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,SK,Kauneudenhoitoala,, -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,SS,Sosiaaliala,, -Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,ST,Sosiaali- ja terveysala,, -Maanpuolustuskorkeakoulu, The National Defence University,Försvarshögskolan,02358,,,,http://isni.org/isni/0000000406476253,1232 -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,,,http://isni.org/isni/0000000119134955,24 -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,180,Rehtori,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,189,Talous- ja hallintopalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,190,Henkilöstöpalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,191,Tietohallintopalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,207,Kirjasto- ja tietopalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,208,Kv-tukipalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,209,Opintoasiainpalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,219,Järjestelmäylläpito,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,220,Käyttäjätuki,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,221,Tiedonhallinta- ja järjestelmäpalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,,01914,,,,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J502A,Aloittavat tutkimusPlatformt,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K850A,Apurahat,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K652A,Asiakaspalvelu,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KD31A,Asiakirjapalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KD43A,Asiakirjapalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B237A,Aurinkotalousprofessuuri,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B220G,Bioenergian laboratorio/Mikkeli,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E122A,Bus yhteiset,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B233A,CDMC-laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B301A,CST,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B24BE,Circular Economy Lahti,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3B0A,Common IEM,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3E3E,"Common, Lahti",, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B360A,Computational and Process Engineering,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3B3A,Cost Management,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K320A,EU-rahoitus,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B21BE,EnTeDI Lahti,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B21AN,EnTeDI Turku,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B210A,Energiatekniikan laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3E0E,Entrepreneurship and management,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J300A,Erityishankkeet,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B202N,FITECH Turku,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E129A,Finance and Business Analytics(FBA),, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J320A,Green Campus Open -kiihdyttämötoiminta,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B371G,Green Chemistry,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KE10A,HR- ja talouspalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KE42A,"Hankinta, tarkastus, verotus, sijoitustoiminta ja",, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KE21A,Henkilöstön kehittämis- ja hyvinvointipalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B251A,Hitsaustekniikan laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K310A,Huipputiederahoitus,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E104A,Hyneman Center,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E123A,IBE,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E121G,IBM/Mikkeli,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K653A,ICT Infra,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3B6A,IEM/Management Research lab,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J410A,IPR ja patentit,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K651A,IS&T + Project office,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K664A,Imatran toimipiste,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B25AE,Industrial Desing Engineering,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3B2A,Innovation Management,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3E1E,Innovation management/LAHTI,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E12DE,International Marketing (IM) Lahti,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E125A,International Marketing(IM),, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E100A,Kanslia,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B30AA,Kemiantekniikan koulutusohjelma,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B241E,Kestävyysmuutoksen laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B242E,Kestävyystiede,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B240A,Kestävät ratkaisut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K860A,Kielikeskus,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KE40A,Kirjanpito ja tilinpäätös,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E12AA,Knowledge Management(KM),, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E12BE,Knowledge Management-Lahti(KM),, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B258A,Konedynamiikka,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B255A,Koneensuunnittelun laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KZ10A,Korkeakoulupalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K830A,Koulutuspalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K821A,Kouluyhteistyö,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3D0D,Kouvola,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B257A,Kuitukomposiittilaboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K666A,LABin kirjastopalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K667A,LAMKin kirjastopalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E124A,LAMO,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E105A,LBM/Management Research lab,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B303A,LENS tekniset palvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J400A,LUT Investointiohjelma,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B250A,LUT Kone yhteiset,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J330A,LUT Oivallus,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B200A,LUT School of Energy Systems yhteiset,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B290A,LUT Voima,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KZ11A,"LUT-yk, KKP",, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K662A,LUTin kirjastopalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KD40A,Lakipalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B253A,Lasertyöstö,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B218A,Laskennallinen virtausmekaniikka,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B30BA,Laskennallisen tekniikan koulutusohjelma,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B211A,Lämpö- ja virtaustekniikan laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E126A,MSIS,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E127A,MSM,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KE30A,Matkustus- ja kv-työskentelyn palvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KD32A,Matkustuspalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B307A,Opetus LENS,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K870A,Opetusteknologia,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K820A,Opiskelijapalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K601A,Oppimis- ja tilapalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K655A,Palvelujärjestelmät,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KD12A,Partneruudet ja verkostot,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3E2E,Performance management/LAHTI,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B370A,Physics,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J504A,Platform DIGI-USER,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E1AAA,Platform LBM Reflex,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E1ADA,Platform LBM DIGI-USER,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E1ACA,Platform LBM RE-SOURCE,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E1AFA,Platform LBM RED,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E1AEA,Platform LBM SAWE,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E1ABA,Platform LBM SIM,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3AAA,Platform LENS Reflex,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3ADA,Platform LENS DIGI-USER,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3ACA,Platform LENS RE-SOURCE,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3AFA,Platform LENS RED,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3AEA,Platform LENS SAWE,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3ABA,Platform LENS SIM,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B2AAA,Platform LES Reflex,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B2ADA,Platform LES DIGI-USER,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B2ACA,Platform LES RE-SOURCE,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B2AFA,Platform LES RED,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B2AEA,Platform LES SAWE,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B2ABA,Platform LES SIM,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J503A,Platform RE-SOURCE,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J506A,Platform RED,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J500A,Platform REFLEX,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J505A,Platform SAWE,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J501A,Platform SIM,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K350A,Projektien hallinta,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KE43A,Projektipalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K300A,Rahoitushakupalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KE20A,Rekrytointi ja työsopimuspalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K663A,Saimian kirjastopalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B309A,School of Engineering Science yhteiset,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E102A,Schoolin yhteiset,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B350A,Separation Science,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KD30A,Sihteeripalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3B1A,Software Engineering,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3C0A,Software Engineering,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B235A,Sovelletun elektroniikan laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KD11A,Strateginen henkilöstö kehittäminen,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KD10A,Strateginen suunnittelu ja toiminnan ohjaus,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E12CA,Strategy and accounting (SA),, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3B5A,Supply Chain Management,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B3B4A,Systems Engineering,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B231A,Sähkömarkkinalaboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B232A,Sähkönkäyttötekniikan laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B230A,Sähkötekniikan laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B234A,Säätö- ja digitaalitekniikan laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KD42A,TES- ja työnantajapalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KE41A,Talouden suunnittelu ja seuranta,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B212A,Teknillinen termodynamiikka,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B256A,Teräsrakenteiden laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K840A,Tietojärjestelmien kehittäminen,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B30CA,Tietotekniikan koulutusohjelma,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K690A,Tilakustannukset,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K691A,Tilakustannukset Saimaa,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K680A,Tilapalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K681A,Tilapalvelut Lahti,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J210A,Toiminnan sunnittelu,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J110A,Toiminta,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B30DA,Tuotantotalouden koulutusohjelma,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B254A,Tuotantotekniikan laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K340A,Tutkimuspolitiikka,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K880A,Täydennyskoulutus liiketoiminta,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K881A,Täydennyskoulutus yhteisrahoitteinen,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B214A,Uusiutuvien energiajärjestelmien laboratorio,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B391J,Varkaus,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23KD20A,Viestintä- ja markkinointipalvelut,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B213A,Virtaustekniikka,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B217A,Ydintekniikka,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K661A,Yhteiskirjasto,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K665A,"Yk-lisät, Kirjasto",, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23E103A,"Yk-lisät, LBM",, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B308A,"Yk-lisät, LENS",, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B201A,"Yk-lisät, LES",, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23J100A,Yliopiston johto,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23K654A,Yliopistopaino,, +Lappeenrannan–Lahden teknillinen yliopisto LUT,LUT University,Lappeenrannan–Lahden teknillinen yliopisto LUT,01914,,23B259A,Älykkäiden koneiden laboratorio,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,,02629,,,,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,Laurea-ammattikorkeakoulu,02629,,SF,Fysioterapia,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,Laurea-ammattikorkeakoulu,02629,,SK,Kauneudenhoitoala,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,Laurea-ammattikorkeakoulu,02629,,LAU,Laurea,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,Laurea-ammattikorkeakoulu,02629,,HL,Liiketalous,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,Laurea-ammattikorkeakoulu,02629,,MP,Matkailu- ja palveluliiketoiminta,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,Laurea-ammattikorkeakoulu,02629,,ST,Sosiaali- ja terveysala,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,Laurea-ammattikorkeakoulu,02629,,SS,Sosiaaliala,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,Laurea-ammattikorkeakoulu,02629,,SH,Terveysala,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,Laurea-ammattikorkeakoulu,02629,,NT,Tietojenkäsittely,, +Laurea-ammattikorkeakoulu,Laurea University of Applied Sciences,Laurea-ammattikorkeakoulu,02629,,HS,Turvallisuusala,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,02446292,,,,http://isni.org/isni/0000000446686757, +Luonnonvarakeskus,Natural Resources Institute Finland,,4100010,,,,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100411610,Aineistopalvelut Haapastensyrjä,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100410510,"Aineistopalvelut Haapastensyrjä, Suonenjoki",, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100411110,Aineistopalvelut Helsinki,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100410610,"Aineistopalvelut Joensuu, Savonlinna",, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100411310,"Aineistopalvelut Jokioinen, Piikkiö kasvintuotanto",, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100411410,"Aineistopalvelut Jokioinen, kotieläintuotanto ja uudet tuotantomuodot",, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100410410,"Aineistopalvelut Kokkola, Seinäjoki",, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100410910,Aineistopalvelut Maaninka,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100411010,"Aineistopalvelut Oulu, Siikajoki",, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100410710,"Aineistopalvelut Rovaniemi, Inari, Utsjoki",, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100411510,Aineistopalvelut Suonenjoki,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100110910,Akvaattisten populaatioiden dynamiikka,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100310110,BITA Esikunta,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100211210,Biojalostusteknologiat ja tuotteet,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100211110,Bioraaka-aineiden rakenne ja ominaisuudet,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100510310,Biotalouden kannattavuus,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100510210,Biotalouden tilastot,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100310610,Biotalous ja ympäristö / Hiilen kierron hallinta (4100310610),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100310810,Biotalous ja ympäristö / Kestävyystutkimus ja indikaattorit (4100310810),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100310210,Biotalous ja ympäristö / Luonnonvarapolitiikat ja -markkinat (4100310210),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100310710,Biotalous ja ympäristö / Maankäyttö ja aluesuunnittelu (4100310710),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100310510,Biotalous ja ympäristö / Metsävarojen inventointi ja metsäsuunnittelu (4100310510),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100310410,Biotalous ja ympäristö / Virkistys ja luontoarvot (4100310410),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100310310,Biotalous ja ympäristö / Yritys- ja ympäristötalous (4100310310),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100311110,Ekosysteemit ja mallinnus,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100211310,Elintarvikkeiden prosessointi ja laatu,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100210310,Eläingenetiikka,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100211510,Eläinravitsemus,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100610110,Esikunta/Pääjohtaja,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100610410,Henkilöstöpalvelut,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100410110,INFRA Esikunta,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100610710,IT ja digitaaliset ratkaisut,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100111110,Kalastus ja kalavarat,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100210210,Kasvigenetiikka,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100110610,Kasvinterveys,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100110110,LUVA Esikunta,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100211010,Liha- ja non-food eläintuotanto,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100110510,Luonnonvarat / Maaperäekosysteemit (4100110510),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100110710,Luonnonvarat / Metsien terveys ja biodiversiteetti (4100110710),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100110310,Luonnonvarat / Metsänhoito (4100110310),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100110210,Luonnonvarat / Peltokasvien tuotanto (4100110210),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100110810,Luonnonvarat / Riistapopulaatioiden dynamiikka (4100110810),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100111010,Luonnonvarat / Soveltava tilastotiede (4100111010),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100110410,Luonnonvarat / Vesistökuormitus (4100110410),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100210710,Maatalouden teknologiat,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100210910,Maidontuotanto,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100210410,Metsänjalostus,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100210610,Metsäteknologia ja logistiikka,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100211410,Nurmet ja kestävä maatalous,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100610610,Palveluryhmät / Talous- ja toimitilat (4100610610),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100610210,Palveluryhmät / Tutkimus ja asiakkuudet (4100610210),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100610510,Palveluryhmät / Viestintä ja markkinointi (4100610510),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100610310,Palveluryhmät / Yhteiskuntasuhteet ja hallinto (4100610310),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100210510,Puutarhateknologiat,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100510110,TIPA Esikunta,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100210110,TUJA Esikunta,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100510410,Tilastopalvelut / Tilastotuotannon menetelmät (4100510410),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100211610,Tuotantoeläinten hyvinvointi,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100210810,Tuotantojärjestelmät / Vesiviljelyratkaisut (4100210810),, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100411210,"Tutkimusinfrastruktuuripalvelut / Aineistopalvelut Jokioinen, Piikkiö (4100411210)",, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100410810,"Tutkimusinfrastruktuuripalvelut / Vesiviljely Enonkoski, Laukaa, Taivalkoski, Keminmaa (4100410810)",, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100111210,Vaelluskalat ja rakennetut joet,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100311010,Ympäristö- ja luonnonvaratalous,, +Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,4100010,,4100310910,Yritystalous ja liiketoimintamallit,, +Lääkealan turvallisuus- ja kehittämiskeskus,The Finnish Medicines Agency,,558005,,,,, +Maanmittauslaitos,National Land Survey of Finland,,4020217,,,,, +Maanpuolustuskorkeakoulu,National Defence University,,02358,,,,, +Maaseutuvirasto,Agency for Rural Affairs in Finland,Landsbygdsverket,24053596,,,,, +Maj ja Tor Nesslingin Säätiö,Maj and Tor Nessling Foundation,,02211186,,,,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,,,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,421,Ajoneuvo- ja konetekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,450,Ajoneuvo- ja konetekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,472,Akuuttihoidon tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,431,Automaatiotekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,476,BiRaYa -tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,461,Clean -tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,463,"ETV, Digi ja Visu -tiimi",, Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,267,Electria,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,293,Taloussuunnittelu- ja seurantapalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,294,Kirjanpito- ja maksuliikennepalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,315,Kiinteistöpalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,319,Koulutuksen kehittämispalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,433,Elektroniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,423,Esittävä taide,, Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,329,Hakijapalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,333,Opintotoimistot,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,340,Viestintäyksikkö,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,341,"Kirjasto- ja tietopalvelut, TeKu-tiimi",, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,343,"Kirjasto- ja tietopalvelut, Sote-tiimi",, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,344,"Kirjasto- ja tietopalvelut, Lite-tiimi",, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,357,Opiskeluhyvinvointitoiminta,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,358,Korkeakoulupalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,400,Liiketoimintapalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,402,Strategia- ja kehityspalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,403,Oppimistoiminta,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,404,TKI-toiminta,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,405,Liiketoiminta,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,406,Tuotantotalous,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,407,Rakentaminen ja arkkitehtuuri,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,408,Puhtaat teknologiat,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,409,Liikkuminen ja toimintakyky,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,478,Hankintapalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,190,Henkilöstöpalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,447,Hyvinvointi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,451,ICT ja tuotantotalous,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,436,"Informaatiotekniikka ja pelisovellukset, tiimi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,477,Jatkuvan oppimisen kehittäminen,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,405,Jatkuvan oppimisen palvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,219,Järjestelmäylläpito,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,418,Kansainvälinen liiketoiminta,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,437,Kehitysjohtajan yksikkö,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,449,Kiinteistö- ja rakennusala,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,454,Kiinteistö- ja talotekniikan tiimi,, Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,410,Kiinteistö- ja talotekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,294,Kirjanpito- ja maksuliikennepalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,207,Kirjasto- ja tietopalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,343,"Kirjasto- ja tietopalvelut, Arabia-Myllypuro",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,344,"Kirjasto- ja tietopalvelut, Karamalmi-Myyrmäki",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,341,"Kirjasto- ja tietopalvelut, TeKu-tiimi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,471,Kliinisen hoidon tiimi,, Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,411,Kliinisen hoitotyön ja ensihoidon palvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,412,Osallistuminen ja toimintakyky,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,413,Terveyden edistämisen palvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,414,Musiikki,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,415,Tieto- ja viestintätekniikka,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,416,Kulttuuripalvelut,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,417,Sähkö- ja automaatiotekniikka,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,418,Kansainvälinen liiketoiminta,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,419,Media,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,420,Terveysalan tutkimuspalvelut ja palvelujohtaminen,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,421,Ajoneuvo- ja konetekniikka,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,422,Sosiaalinen hyvinvointi,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,423,Esittävä taide,, Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,424,Konservointi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,465,"Konservointi, 3D, XR -tiimi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,358,Korkeakoulupalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,416,Kulttuuripalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,444,Kulttuuripalvelut ja musiikki,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,460,Kulttuurituotannon ja vaatetuksen tiimi,, Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,425,Kulttuurituotanto,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,426,Muotoilu,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,427,Vaatetus,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,428,"Kv-liiketoiminta, hallinto",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,474,Kuntoutuksen tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,446,Kuntoutus ja tutkiminen,, Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,429,"Kv-liiketoiminta, YAMK ja TKI",, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,430,Sähkövoimatekniikka,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,431,Automaatiotekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,428,"Kv-liiketoiminta, hallinto",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,208,Kv-tukipalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,220,Käyttäjätuki,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,442,Laki- ja arkistointipalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,468,Liiketalouden AMK-tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,469,Liiketalouden YAMK- ja KV-tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,452,Liiketalous,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,409,Liikkuminen ja toimintakyky,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,457,Me3 -tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,419,Media,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,443,"Media, muotoilu ja konservointi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,435,Mediatekniikan tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,426,Muotoilu,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,464,"Muotoilu, teknot ja yleisaineet -tiimi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,470,Musiikin tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,414,Musiikki,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,438,Näyttökeskus,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,209,Opintoasiainpalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,333,Opiskelija- ja hakijapalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,357,Opiskeluhyvinvointitoiminta,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,403,Oppimistoiminta,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,319,Oppimistoimintapalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,412,Osallistuminen ja toimintakyky,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,459,Palkanlaskentapalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,448,Puhtaat ja älykkäät ratkaisut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,408,Puhtaat teknologiat,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,475,Pääasia -tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,407,Rakentaminen ja arkkitehtuuri,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,455,Rakentaminen ja arkkitehtuuri -tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,180,Rehtorin yksikkö,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,462,Smart -tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,458,"Softa, Hyte ja Smart -tiimi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,467,Sosiaalialan tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,422,Sosiaalinen hyvinvointi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,402,Strategia- ja kehityspalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,417,Sähkö- ja automaatiotekniikka,, Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,432,"Sähkö- ja automaatiotekniikka, TKi",, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,433,Elektroniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,430,Sähkövoimatekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,453,TKI-palvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,404,TKI-toiminta,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,189,Talous- ja hallintopalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,293,Taloussuunnittelu- ja seurantapalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,413,Terveyden edistämisen palvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,473,Terveyden edistämisen tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,445,Terveys,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,420,Terveysalan tutkimuspalvelut ja palvelujohtaminen,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,221,Tiedonhallinta- ja järjestelmäpalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,415,Tieto- ja viestintätekniikka,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,191,Tietohallintopalvelut,, Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,434,Tietoverkkotekniikan tiimi,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,435,Mediatekniikan tiimi,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,436,"Informaatiotekniikka ja pelisovellukset, tiimi",, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,437,Kehitysjohtajan yksikkö,, -Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,438,Näyttökeskus,, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,,,http://isni.org/isni/0000000094586751,1473 -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10200,Hallintoyksikkö,, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10210,Kehittäminen,, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10211,Viestintä,, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10212,Hallintopalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,315,Toimitilapalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,456,Tuotantotalouden tiimi,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,406,Tuotantotalous,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,400,Täydennyskoulutus- ja yrityspalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,479,Täydennyskoulutus- ja yrityspalvelut,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,,10065,,427,Vaatetus,, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,466,"Vanhustyö, kuntoutus, toimintaterapia -tiimi",, +Metropolia ammattikorkeakoulu,Metropolia University of Applied Sciences,Metropolia ammattikorkeakoulu,10065,,340,Viestintäyksikkö,, +Metsäntutkimuslaitos,Finnish Forest Research Institute,Skogsforskningsinstitutet,404001,,,,http://isni.org/isni/0000000122651136,TWV0c8OkbnR1dGtpbXVzbGFpdG9z +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,,,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102200,Ammatillinen opettajakoulutus,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,104400,Ammatillisen opettajankoulutuksen yksikkö,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102600,Energia ja automaatio,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102120,Hallintopalvelut,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,104000,Hallintoyksikkö,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102000,Hallintoyksikkö,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102300,Hoitoalat (Oulu ja Oulainen),, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102750,Informaatioteknologia,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,104800,Informaatioteknologian yksikkö,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102100,Kehittäminen,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102700,Konetekniikka,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102900,Kulttuuri,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,104900,Kulttuurialan yksikkö,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,104600,Liiketalouden yksikkö,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102500,Liiketalous,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102800,Luonnonvara-ala,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102650,Rakentamistekniikka,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,104500,Sosiaali- ja terveysalan yksikkö,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102400,Sosiaaliala ja kuntoutus,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,104700,Tekniikan ja Luonnonvara-alan yksikkö,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102350,Terveydenhuollon erityisalat,, +Oulun ammattikorkeakoulu,Oulu University of Applied Sciences,Oulun ammattikorkeakoulu,02471,,102110,"Viestintä, markkinointi ja aluevaikuttavuus",, Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10220,Ammatillinen opettajakoulutus,, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10230,Hoitoalat (Oulu ja Oulainen),, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10235,Terveydenhuollon erityisalat,, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10240,Sosiaaliala ja kuntoutus,, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10250,Liiketalous,, Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10260,Energia ja automaatio,, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10265,Rakentamistekniikka,, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10270,Konetekniikka,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10212,Hallintopalvelut,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10200,Hallintoyksikkö,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10230,Hoitoalat (Oulu ja Oulainen),, Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10275,Informaatioteknologia,, -Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10280,Luonnonvara-ala,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10210,Kehittäminen,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10270,Konetekniikka,, Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10290,Kulttuuri,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,,,http://isni.org/isni/0000000404505858,1478 -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24000,HUMANISTINEN TIEDEKUNTA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240000,Humanistinen tiedekunta,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240000,2400000,Humanistinen tiedekunta yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240000,2400001,HuTK koulutus,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10250,Liiketalous,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10280,Luonnonvara-ala,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10265,Rakentamistekniikka,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10240,Sosiaaliala ja kuntoutus,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10235,Terveydenhuollon erityisalat,, +Oulun seudun ammattikorkeakoulu,Oulu University of Applied Sciences,,02471,,10211,Viestintä,, +Oulun yliopisto,University of Oulu,,01904,,,,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2406140,6Genesis Flagship,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240614,6Genesis Flagship,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240807,AIKOPA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408071,AIKOPA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24051,ARKKITEHTUURIN TIEDEKUNTA,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240030,Aate- ja oppihistoria,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240030,2400300,Aate- ja oppihistoria,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400331,Aate- ja oppihistoria,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240840,Aikuiskoulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408400,Aikuiskoulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2406130,Allied ICT Finland,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240613,Allied ICT Finland,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2407310,Alueellinen erinomaisuus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240731,Alueellinen erinomaisuus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2430,Alueyksiköt,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408061,Analyyttinen kemia/bioanalytiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240041,Arkeologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240041,2400410,Arkeologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400335,Arkeologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240510,Arkkitehtuuri,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405100,Arkkitehtuuri,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24051,240510,Arkkitehtuurin tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240510,2405100,Arkkitehtuurin tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240770,2407701,Arktinen lääketiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409390,Asiakaspalvelu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409305,Asiakirjapalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409140,Aulapalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409125,Autopaikat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240225,Avaruusfysiikan ja tähtitieteen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402250,Avaruusfysiikan ja tähtitieteen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240215,Avaruusilmasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402150,Avaruusilmasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240840,2408406,Avoin yliopisto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24031,BIOKEMIAN JA MOLEKYYLILÄÄKETIETEEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24031,BIOKEMIAN JA MOLEKYYLILÄÄKETIETEEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24066,Biocenter,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240681,Biocenter,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2406810,Biocenter,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24067,Biocenter Core,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240670,Biocenter Core,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2406700,Biocenter Core,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240262,Biocomputing & COPD,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402620,Biocomputing & COPD,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24089,Biodiversiteettiyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240212,Biodiversiteettiyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402120,Biodiversiteettiyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240201,Biokemian ja molekyylilääketieteen tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402010,Biokemian ja molekyylilääketieteen tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240230,Biologian ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402300,Biologian ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240342,Biolääketieteellinen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403420,Biolääketieteellinen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240267,Biomedical structural biology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402670,Biomedical structural biology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240599,Biomimetiikka ja älykkäät järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405990,Biomimetiikka ja älykkäät järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240343,Biopankki Borealis,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403430,Biopankki Borealis,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240585,Biosignaalien analyysi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240585,2405850,Biosignaalien analyysi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405851,Biosignaalien analyysi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240775,CEE Innovaatiokeskittymä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240775,2407750,CEE Innovaatiokeskittymä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24080,240806,CEMIS-Oulu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240806,2408060,CEMIS-Oulu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240309,CERH,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403090,CERH,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403007,CHT,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240700,CIE 31.12.2015 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240587,CWC - Radioteknologiat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405870,CWC - Radioteknologiat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240612,CWC - Verkot ja järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2406120,CWC - Verkot ja järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402740,Cancer Genomics,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240274,Cancer Genomics,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240271,Cell-matrix biology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402710,Cell-matrix biology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240720,Center for Health and Technology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240720,2407200,Center for Health and Technology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409690,Controller lähipalvelutiimi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240969,Controller lähipalvelutiimi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240273,Disease Networks,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402730,Disease Networks,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240334,ELITE,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403340,ELITE,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240260,Eklund research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240260,2402600,Eklund research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240213,Ekologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240213,2402130,Ekologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402130,Ekologian ja genetiikan tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240213,Ekologian ja genetiikan tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240583,Elektroniikan piirit ja järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405830,Elektroniikan piirit ja järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240208,"Empiirinen ohjelmistotuotanto ohjelmistoissa, jär",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402080,"Empiirinen ohjelmistotuotanto ohjelmistoissa, jär",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240208,"Empiirinen ohjelmistotuotanto ohjelmistoissa, järjestelmissä ja palveluissa",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240208,2402080,"Empiirinen ohjelmistotuotanto ohjelmistoissa, järjestelmissä ja palveluissa",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240548,Energia- ja ympäristötekniikan tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240548,2405480,Energia- ja ympäristötekniikan tutkimusryhmä,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240031,Englantilainen filologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240031,2400310,Englantilainen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400310,Englantilainen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240216,Epäorgaaninen kemia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240216,2402160,Epäorgaaninen kemia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2440,Erillisyksiköt,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24064,Eudaimonia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240640,Eudaimonia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2406400,Eudaimonia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240408,Executive Education,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2404080,Executive Education,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240542,FabLab TTK,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405420,FabLab TTK,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405943,Fablab Infrastruktuuri,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240231,Fysiikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402310,Fysiikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240214,Genetiikka ja fysiologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240214,2402140,Genetiikka ja fysiologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24025,240250,Geotieteen tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240250,2402500,Geotieteen tutkimusryhmä,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240032,Germaaninen filologia,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240032,2400320,Germaaninen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400311,Germaaninen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240020,Giellagas-instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400200,Giellagas-instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402650,Glycan biosynthesis,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240265,Glycan biosynthesis,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24000,HUMANISTINEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240970,Hallituksen vararahasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409700,Hallituksen vararahasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403003,Hammaslääketieteen ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409920,Hankinnat ja kilpailutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240992,Hankinnat ja kilpailutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408101,Hankintapalvelu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409160,Hankintapalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24192,Henkilöstö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240941,2409471,Henkilöstöhallinto keskitetyt palvelut (KEPA),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240922,Henkilöstöpalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409220,Henkilöstöpalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240941,Henkilöstöpalvelut lähipalvelutiimi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409471,Henkilöstöpalvelut lähipalvelutiimi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240261,Hiltunen research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240261,2402610,Hiltunen research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240033,Historia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240033,2400330,Historia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400330,Historia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240033,"Historian, kulttuurin ja viestintätieteiden tutki",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403390,Hoitotieteen ja terveyshallintotieteen tutkimusyks,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240339,Hoitotieteen ja terveyshallintotieteen tutkimusyks,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240339,Hoitotieteen ja terveyshallintotieteen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240339,2403390,Hoitotieteen ja terveyshallintotieteen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400001,HuTK koulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240000,Humanistinen tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400000,Humanistinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402700,Hypoxia & collagens,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240270,Hypoxia & collagens,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402630,Hypoxia response,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240263,Hypoxia response,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409380,ICT käyttöpalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24197,ICT-palvelut,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240034,Informaatiotutkimus ja viestintä,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240034,2400340,Informaatiotutkimus ja viestintä,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240034,2400341,Tiedeviestinnän maisteriohjelma,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240035,Kirjallisuus ja elokuvatutkimus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240035,2400350,Kirjallisuus ja elokuvatutkimus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240036,Kulttuuriantropologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240036,2400360,Kulttuuriantropologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240037,Logopedia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240037,2400370,Logopedia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240038,Pohjoismainen filologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240038,2400380,Pohjoismainen filologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240039,Suomen kieli,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240039,2400390,Suomen kieli,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240041,Arkeologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240041,2400410,Arkeologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240020,Giellagas-instituutti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240020,2400200,Giellagas-instituutti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24010,KASVATUSTIETEIDEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400332,Informaatiotutkimus ja viestintä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24068,Infotech,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240680,Infotech,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2406800,Infotech,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2445,Infrastruktuuri,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240949,Innovaatio- ja yrittäjyyspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240949,2409245,Innovaatio- ja yrittäjyyspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24290,Innovaatiokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402741,Integrin functions,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409492,International Avenue,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240402,Johtamisen ja kansainvälisen liiketoiminnan yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240402,2404020,Johtamisen ja kansainvälisen liiketoiminnan yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24092,Johto ja palvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2460,Johto ja palvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409210,Johto ja palvelut - yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240921,Johto ja palvelut - yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240611,Jokapaikan tietotekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2406110,Jokapaikan tietotekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240262,Juffer and Ohlmeier research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240262,2402620,Juffer and Ohlmeier research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24025,KAIVANNAISALAN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24010,KASVATUSTIETEIDEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24011,KASVATUSTIETEIDEN TIEDEKUNTA,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24010,24011,KASVATUSTIETEIDEN TIEDEKUNTA ilman harjoittelukouluja,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24011,240100,Kasvatustieteiden tiedekunta yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240100,2401000,Kasvatustieteiden tiedekunta yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240100,2401001,KTK koulutus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24011,240103,Oppiminen ja oppimisprosessit,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240103,2401030,Oppiminen ja oppimisprosessit,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24011,240104,"Opettajat, opettaminen ja kasvatusyhteisöt",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240104,2401040,"Opettajat, opettaminen ja kasvatusyhteisöt",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24011,240105,"Kasvatuksen arvot, aatteet ja yhteiskunnalliset kontekstit",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240105,2401050,"Kasvatuksen arvot, aatteet ja yhteiskunnalliset kontekstit",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24010,24015,Oulun yliopiston harjoittelukoulut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24015,240150,Oulun yliopiston harjoittelukoulu,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240150,2401500,Oulun yliopiston harj.koulu yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24015,240151,"Oulun normaalikoulu, Linnanmaa (0-6)",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240151,2401510,"Oulun normaalikoulu, Linnanmaa (0-6)",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24015,240152,"Oulun normaalikoulu, Linnanmaa (7-9)",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240152,2401520,"Oulun normaalikoulu, Linnanmaa (7-9)",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24015,240153,"Oulun normaalikoulu, Koskela 1-6",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240153,2401530,"Oulun normaalikoulu, Koskela 1-6",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24015,240155,"Oulun normaalikoulu, lukio",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240155,2401550,"Oulun normaalikoulu, lukio",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24020,LUONNONTIETEELLINEN TIEDEKUNTA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240200,Luonnontieteellinen tiedekunta yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240200,2402000,Luonnontieteellinen tiedekunta yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240211,Kemikaalipalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240211,2402110,Kemikaalipalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240206,Maantiede,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240206,2402060,Maantiede,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240213,Ekologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240213,2402130,Ekologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240214,Genetiikka ja fysiologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240214,2402140,Genetiikka ja fysiologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240215,Avaruusilmasto,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240215,2402150,Avaruusilmasto,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240216,Epäorgaaninen kemia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240216,2402160,Epäorgaaninen kemia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240217,Kestävä kemia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240217,2402170,Kestävä kemia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240207,Matematiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240207,2402070,Matematiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240218,Sovellettu ja laskennallinen matematiikka 31.7.2016 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240218,2402180,Sovellettu ja laskennallinen matematiikka 31.7.2016 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240219,Sovellettu matematiikka ja tilastotiede,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240219,2402190,Sovellettu matematiikka ja tilastotiede,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240220,Neurobiofysiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240220,2402200,Neurobiofysiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240221,Molekylaariset järjestelmät,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240221,2402210,Molekylaariset järjestelmät,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240222,NMR Spectroskopia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240222,2402220,NMR Spectroskopia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240223,Teoreettinen fysiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240223,2402230,Teoreettinen fysiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240224,Tähtitiede,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240224,2402240,Tähtitiede,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240225,Ionosfäärifysiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240225,2402250,Ionosfäärifysiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240230,Biologian ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240230,2402300,Biologian ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240231,Fysiikan ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240231,2402310,Fysiikan ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240232,Kemian ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240232,2402320,Kemian ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240233,Maantieteen ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240233,2402330,Maantieteen ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240234,Matematiikan ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240234,2402340,Matematiikan ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240234,2402341,OuLUMA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24025,KAIVANNAISALAN TIEDEKUNTA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24025,240250,Geotieteen tutkimusryhmä,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240250,2402500,Geotieteen tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240335,KNK- ja silmätautien tutkimusyksikkö 31.1.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240335,2403350,KNK- ja silmätautien tutkimusyksikkö 31.1.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240731,KSI Aluekehittäminen,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240731,2407310,KSI Aluekehittäminen,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240734,KSI Maanalainen fysiikka (CUPP),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240734,2407340,KSI Maanalainen fysiikka (CUPP),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240733,KSI Mikroyrittäjyys (MikroY),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2407330,KSI Mikroyrittäjyys (MikroY),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240736,KSI Raahe,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240736,2407360,KSI Raahe,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240732,KSI Tulevaisuuden tuotantoteknologiat (FMT),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2407320,KSI Tulevaisuuden tuotantoteknologiat (FMT),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2401001,KTK koulutus,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240250,2402501,KaTK koulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402500,Kaivannaisala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240250,Kaivannaisala,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24025,240251,Kaivos- ja rikastustekniikan tutkimusryhmä,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240251,2402510,Kaivos- ja rikastustekniikan tutkimusryhmä,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24030,LÄÄKETIETEELLINEN TIEDEKUNTA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240300,Lääketieteellinen tiedekunta yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240300,2403000,Lääketieteellinen tiedekunta yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240300,2403002,Lääketieteen ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240300,2403003,Hammaslääketieteen ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240300,2403004,Terveystieteiden ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240305,Suun terveyden tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240305,2403050,Suun terveyden tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240309,CERH,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240309,2403090,CERH,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240331,Medical Research Center (MRC),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240331,2403310,Medical Research Center (MRC),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240332,Pohjois-Suomen syntymäkohortti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240332,2403320,Pohjois-Suomen syntymäkohortti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240333,Sisätautien tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240333,2403330,Sisätautien tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240334,ELITE,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240334,2403340,ELITE,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240335,KNK- ja silmätautien tutkimusyksikkö 31.1.2016 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240335,2403350,KNK- ja silmätautien tutkimusyksikkö 31.1.2016 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240336,Neurotieteen tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240336,2403360,Neurotieteen tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240337,PEDEGO-tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240337,2403370,PEDEGO-tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240338,"Kirurgian, anestesiologian ja tehohoidon tutkimusyksikkö",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240338,2403380,"Kirurgian, anestesiologian ja tehohoidon tutkimusyksikkö",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240339,Hoitotieteen ja terveyshallintotieteen tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240339,2403390,Hoitotieteen ja terveyshallintotieteen tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240340,"Lääketieteellisen kuvantamisen, fysiikan ja tekniikan tutkimusyksikkö",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240340,2403400,"Lääketieteellisen kuvantamisen, fysiikan ja tekniikan tutkimusyksikkö",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240341,Syövän ja translationaalisen lääketieteen tutkimyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240341,2403410,Syövän ja translationaalisen lääketieteen tutkimyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240342,Biolääketieteellinen tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240342,2403420,Biolääketieteellinen tutkimusyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240343,Biopankki Borealis,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240343,2403430,Biopankki Borealis,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24040,OULUN YLIOPISTON KAUPPAKORKEAKOULU,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240400,Oulun yliopiston kauppakorkeakoulu,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240400,2404000,Oulun yliopiston kauppakorkeakoulu yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240402,Johtamisen ja kansainvälisen liiketoiminnan yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240402,2404020,Johtamisen ja kansainvälisen liiketoiminnan yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240403,Taloustieteen yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240403,2404030,Taloustieteen yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240404,Laskentatoimen yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240404,2404040,Laskentatoimen yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240405,Markkinoinnin yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240405,2404050,Markkinoinnin yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240406,Rahoituksen yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240406,2404060,Rahoituksen yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240407,Martti Ahtisaari instituutti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240407,2404070,Martti Ahtisaari Instituutti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240408,Executive Education,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240408,2404080,Executive Education,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24050,TEKNILLINEN TIEDEKUNTA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240500,Teknillinen tiedekunta yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240500,2405000,Teknillinen tiedekunta yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240541,Tiedekuntalaboratorio,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240541,2405410,Tiedekuntalaboratorio,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240542,FabLab TTK,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240542,2405420,FabLab TTK,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240543,Konetekniikan ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240543,2405430,Konetekniikan ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240544,Prosessi- ja ympäristötekniikan ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240544,2405440,Prosessi- ja ympäristötekniikan ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240545,Tuotantotalouden ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240545,2405450,Tuotantotalouden ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240546,Tohtorikoulutettavat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240546,2405460,Tohtorikoulutettavat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240547,Opetustilat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240547,2405470,Opetustilat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240548,Energia- ja ympäristötekniikan tutkimusryhmä,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240548,2405480,Energia- ja ympäristötekniikan tutkimusryhmä,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240538,Työpaja,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240538,2405380,Työpaja,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240550,Prosessi- ja ympäristötekniikan osasto (ei käytössä),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240550,2405500,Prosessi- ja ympäristötekniikan os. yht. (ei käytössä),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240550,2405610,Minipilot-rikastamo (ei käytössä),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240532,Mekatroniikka ja konediagnostiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240532,2405320,Mekatroniikka ja konediagnostiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240533,Koneensuunnittelu,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240533,2405330,Koneensuunnittelu,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240534,Materiaali- ja tuotantotekniikan tutkimusryhmä,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240534,2405340,Materiaali- ja tuotantotekniikan tutkimusryhmä,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240536,Rakenteet ja rakentamisteknologia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240536,2405360,Rakennesuunnittelun ja rakentamisteknologian tutkimusryhmä,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240552,Vesi- ja ympäristötekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240552,2405520,Vesi- ja ympäristötekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240554,Kemiallinen prosessitekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240554,2405540,Kemiallinen prosessitekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240555,Ympäristö- ja kemiantekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240555,2405550,Ympäristö- ja kemiantekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240557,Kuitu- ja partikkelitekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240557,2405570,Kuitu- ja partikkelitekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240558,Säätötekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240558,2405580,Säätötekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240559,Systeemitekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240559,2405590,Systeemitekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240560,Prosessimetallurgia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240560,2405600,Prosessimetallurgia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240570,Tuotantotalous,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240570,2405700,Tuotantotalous,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240579,Konepaja- ja metalliosaamisen innovaatiokeskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240579,2405790,Konepaja- ja metalliosaamisen innovaatiokeskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24031,BIOKEMIAN JA MOLEKYYLILÄÄKETIETEEN TIEDEKUNTA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240201,Biokemian ja molekyylilääketieteen tiedekunta,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240201,2402010,Biokemian ja molekyylilääketieteen tiedekunta,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240260,Eklund research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240260,2402600,Eklund research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240261,Hiltunen research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240261,2402610,Hiltunen research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240262,Juffer and Ohlmeier research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240262,2402620,Juffer and Ohlmeier research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24080,Kajaanin yliopistokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240800,Kajaanin yliopistokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408000,Kajaanin yliopistokeskus yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409155,Kalusteasentajat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240925,Kampuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24198,Kampuspalvelut,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240263,Karppinen research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240263,240002630,Karppinen research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240264,Kastaniotis research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240264,2402640,Kastaniotis research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240105,"Kasvatuksen arvot, aatteet ja yhteiskunnalliset ko",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2401050,"Kasvatuksen arvot, aatteet ja yhteiskunnalliset ko",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24011,240105,"Kasvatuksen arvot, aatteet ja yhteiskunnalliset kontekstit",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240105,2401050,"Kasvatuksen arvot, aatteet ja yhteiskunnalliset kontekstit",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240100,Kasvatustieteiden tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2401000,Kasvatustieteiden tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409391,Kehittämispalvelut,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240265,Kellokumpu and Glumoff research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240265,2402650,Kellokumpu and Glumoff research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240554,Kemiallinen prosessitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405540,Kemiallinen prosessitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240232,Kemian ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240232,2402320,Kemian ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240540,Kemian ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405400,Kemian ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240211,Kemikaalipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240211,2402110,Kemikaalipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24073,Kerttu Saalasti instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240730,Kerttu Saalasti instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2407300,Kerttu Saalasti instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240217,Kestävä kemia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240217,2402170,Kestävä kemia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405530,Kestävä kemia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240553,Kestävä kemia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240820,Kieli- ja viestintäkoulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408200,Kieli- ja viestintäkoulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240031,Kielten ja kirjallisuuden tutkimusyksikkö,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240266,Kietzmann research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240266,2402660,Kietzmann research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409250,Kiinteistö- ja toimitilajohtaminen,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240911,Kiinteistöt,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409110,Kiinteistöt,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240035,Kirjallisuus ja elokuvatutkimus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240035,2400350,Kirjallisuus ja elokuvatutkimus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400312,Kirjallisuus ja elokuvatutkimus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24081,Kirjasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240810,Kirjasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408100,Kirjasto yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408150,Kirjaston hallintopalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403380,"Kirurgian, anestesiologian ja tehohoidon tutkimusy",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240338,"Kirurgian, anestesiologian ja tehohoidon tutkimusy",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240338,"Kirurgian, anestesiologian ja tehohoidon tutkimusyksikkö",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240338,2403380,"Kirurgian, anestesiologian ja tehohoidon tutkimusyksikkö",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24083,Koe-eläinkeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240830,Koe-eläinkeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408300,Koe-eläinkeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240533,Koneensuunnittelu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240533,2405330,Koneensuunnittelu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240584,Konenäkö ja signaalianalyysi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405840,Konenäkö ja signaalianalyysi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240579,Konepaja- ja metalliosaamisen innovaatiokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240579,2405790,Konepaja- ja metalliosaamisen innovaatiokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240543,Konetekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405430,Konetekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24193,Koulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408104,Koulutus- ja tietopalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409230,Koulutuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240923,Koulutuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409412,Koulutuspalvelut HKT 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409432,Koulutuspalvelut Kontinkangas 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409422,Koulutuspalvelut LUTK 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409930,Koulutuspalvelut OAMK lähipalvelutiimi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240993,Koulutuspalvelut OAMK lähipalvelutiimi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409442,Koulutuspalvelut TTK 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409660,"Koulutuspalvelut lähipalvelutiimi KTK, HuTK, OyKK",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240966,"Koulutuspalvelut lähipalvelutiimi KTK, HuTK, OyKK",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409670,"Koulutuspalvelut lähipalvelutiimi LTK, BMTK",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240967,"Koulutuspalvelut lähipalvelutiimi LTK, BMTK",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409680,Koulutuspalvelut lähipalvelutiimi TSTK,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240968,Koulutuspalvelut lähipalvelutiimi TSTK,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240965,Koulutuspalvelut lähipalvelutiimi TTK ja LuTK,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409650,Koulutuspalvelut lähipalvelutiimi TTK ja LuTK,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240842,Koulutusvienti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408420,Koulutusvienti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240557,Kuitu- ja partikkelitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405570,Kuitu- ja partikkelitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240036,Kulttuuriantropologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240036,2400360,Kulttuuriantropologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400334,Kulttuuriantropologia,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240267,Kursula research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240267,2402670,Kursula research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408062,Kuvantavat mittaukset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2417000,Kvantum Institute,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,241700,Kvantum Institute,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24170,Kvantum Institute,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405970,Käyttöliittymät ja ihmiskeskeinen digitalisaati,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240597,Käyttöliittymät ja ihmiskeskeinen digitalisaati,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240597,Käyttöliittymät ja ihmiskeskeinen digitalisaatio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240597,2405970,Käyttöliittymät ja ihmiskeskeinen digitalisaatio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24088,LEAF,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240778,LEAF,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2407780,LEAF,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24020,LUONNONTIETEELLINEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408103,Lainaus- ja neuvontapalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24191,Laki- ja sopimuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240404,Laskentatoimen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240404,2404040,Laskentatoimen yksikkö,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240268,Lehtiö research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240268,2402680,Lehtiö research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240037,Logopedia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400370,Logopedia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240200,Luonnontieteellinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402000,Luonnontieteellinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24030,LÄÄKETIETEELLINEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24093,Lähipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240300,Lääketieteellinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403000,Lääketieteellinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403400,"Lääketieteellisen kuvantamisen, fysiikan ja tekn",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240340,"Lääketieteellisen kuvantamisen, fysiikan ja tekn",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240340,"Lääketieteellisen kuvantamisen, fysiikan ja tekniikan tutkimusyksikkö",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240340,2403400,"Lääketieteellisen kuvantamisen, fysiikan ja tekniikan tutkimusyksikkö",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403002,Lääketieteen ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408060,MITY,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240806,MITY,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240206,Maantiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402060,Maantiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240233,Maantieteen ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240233,2402330,Maantieteen ala,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240269,Manninen research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240269,2402690,Manninen research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240405,Markkinoinnin yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240405,2404050,Markkinoinnin yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240402,"Markkinoinnin, johtamisen ja kansainvälisen liike",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2404020,"Markkinoinnin, johtamisen ja kansainvälisen liike",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2404070,Martti Ahtisaari Instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240407,Martti Ahtisaari instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402070,Matemaattisten tieteiden tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240207,Matemaattisten tieteiden tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240234,Matematiikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402340,Matematiikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240207,Matematiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240207,2402070,Matematiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240534,Materiaali- ja konetekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405340,Materiaali- ja konetekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240534,Materiaali- ja tuotantotekniikan tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240534,2405340,Materiaali- ja tuotantotekniikan tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24079,Materiaalianalyysikeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2407900,Materiaalianalyysikeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240790,Materiaalianalyysikeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240331,Medical Research Center (MRC),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403310,Medical Research Center (MRC),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240532,Mekatroniikka ja konediagnostiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240532,2405320,Mekatroniikka ja konediagnostiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240586,Mikroelektroniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405860,Mikroelektroniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24079,Mikroskopian ja nanoteknologian keskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24079,240790,Mikroskopian ja nanoteknologian keskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240790,2407900,Mikroskopian ja nanoteknologian keskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240550,2405610,Minipilot-rikastamo (ei käytössä),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240264,Mitochondria and lipids,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402640,Mitochondria and lipids,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240221,Molekylaariset järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240221,2402210,Molekylaariset järjestelmät,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240270,Myllyharju research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240270,2402700,Myllyharju research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240222,NMR Spectroskopia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240222,2402220,NMR Spectroskopia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240222,NMR-spektroskopia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402220,NMR-spektroskopia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240972,NN rahasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409720,NN rahasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402210,Nano- ja molekyylisysteemien tutkimusyksikkö (NAN,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240221,Nano- ja molekyylisysteemien tutkimusyksikkö (NAN,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240220,Neurobiofysiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240220,2402200,Neurobiofysiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240336,Neurotieteen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403360,Neurotieteen tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240773,NorTech 31.12.2015 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240252,OMS Tutkimuskeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402520,OMS Tutkimuskeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24040,OULUN YLIOPISTON KAUPPAKORKEAKOULU,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24095,Omakatteiset rahastot,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240104,"Opettajat, opettaminen ja kasvatusyhteisöt",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2401040,"Opettajat, opettaminen ja kasvatusyhteisöt",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409385,Opetuksen IT-palvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409385,Opetuksen ja tutkimuksen IT-tukipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409236,Opetuksen tuki,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240547,Opetustilat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405470,Opetustilat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409232,Opintotuki (entinen työelämäpalvelut) 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409237,Opiskelijavalinta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409239,Opiskelun tuki,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409238,Opiskeluoikeuden ylläpito,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240103,Oppiminen ja oppimisprosessit,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2401030,Oppiminen ja oppimisprosessit,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2408063,Optinen spektroskopia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240582,Optoelektroniikka ja mittaustekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405820,Optoelektroniikka ja mittaustekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402341,OuLUMA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24085,Oulangan tutkimusasema,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240771,Oulangan tutkimusasema,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2407710,Oulangan tutkimusasema,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240153,"Oulun normaalikoulu, Koskela 1-6",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2401530,"Oulun normaalikoulu, Koskela 1-6",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240151,"Oulun normaalikoulu, Linnanmaa (0-6)",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2401510,"Oulun normaalikoulu, Linnanmaa (0-6)",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240152,"Oulun normaalikoulu, Linnanmaa (7-9)",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2401520,"Oulun normaalikoulu, Linnanmaa (7-9)",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240155,"Oulun normaalikoulu, lukio",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2401550,"Oulun normaalikoulu, lukio",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24,Oulun yliopisto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240950,Oulun yliopiston apuraharahasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409500,Oulun yliopiston apuraharahasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2401500,Oulun yliopiston harj.koulu yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240150,Oulun yliopiston harjoittelukoulu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24015,Oulun yliopiston harjoittelukoulut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240400,Oulun yliopiston kauppakorkeakoulu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2404000,Oulun yliopiston kauppakorkeakoulu yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240266,Oxygen sensing,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402660,Oxygen sensing,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240337,PEDEGO-tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403370,PEDEGO-tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402760,PROFI3,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240276,PROFI3,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2420,Painoalat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240538,Paja,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405380,Paja,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240941,Palvelupisteet henkilöstöhallinto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240942,Palvelupisteet koulutuspalvelut 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240943,Palvelupisteet talouspalvelut,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240271,Pihlajaniemi and Heljasvaara research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240271,2402710,Pihlajaniemi and Heljasvaara research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240332,Pohjois-Suomen syntymäkohortti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403320,Pohjois-Suomen syntymäkohortti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240038,Pohjoismainen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240038,2400380,Pohjoismainen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400313,Pohjoismainen filologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409150,Postipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409840,Private Equity rahasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240984,Private Equity rahasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240952,Professori Pentti Kaiteran rahasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409520,Professori Pentti Kaiteran rahasto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240964,Projektitalous,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409640,Projektitalous,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405440,Prosessi- ja ympäristötekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240544,Prosessi- ja ympäristötekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240550,2405500,Prosessi- ja ympäristötekniikan os. yht. (ei käytössä),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240550,Prosessi- ja ympäristötekniikan osasto (ei käytössä),, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240560,Prosessimetallurgia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405600,Prosessimetallurgia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240272,Protein and Structural Biology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402720,Protein and Structural Biology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2470,Rahastot,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240406,Rahoituksen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240406,2404060,Rahoituksen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240536,2405360,Rakennesuunnittelun ja rakentamisteknologian tutkimusryhmä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405490,Rakennus- ja yhdyskuntatekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240549,Rakennus- ja yhdyskuntatekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409130,Rakentaminen ja kunnossapito,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240536,Rakenteet ja rakentamisteknologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405360,Rakenteet ja rakentamisteknologia,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240900,Rehtoraatin yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409000,Rehtoraatin yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409230,Resurssipooli,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240272,Ruddock research group,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240272,2402720,Ruddock research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240273,Vainio research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240273,2402730,Vainio research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240274,Wei research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240274,2402740,Wei research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240275,Wierenga and Venkatesan research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240275,2402750,Wierenga and Venkatesan research group,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24051,ARKKITEHTUURIN TIEDEKUNTA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24051,240510,Arkkitehtuurin tiedekunta,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240510,2405100,Arkkitehtuurin tiedekunta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240760,SGO Havaintotoiminta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2407600,SGO Havaintotoiminta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240762,SGO Tutkimus- ja kehitystoiminta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2407620,SGO Tutkimus- ja kehitystoiminta,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24098,Sijoitusvarallisuus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240333,Sisätautien tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403330,Sisätautien tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24076,Sodankylän geofysiikan observatorio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24076,240760,Sodankylän geofysiikan observatorio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240760,2407600,Sodankylän geofysiikan observatorio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240589,Sovellettu ja laskennallinen matematiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405890,Sovellettu ja laskennallinen matematiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240218,Sovellettu ja laskennallinen matematiikka 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240218,2402180,Sovellettu ja laskennallinen matematiikka 31.7.2016 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240219,Sovellettu matematiikka ja tilastotiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240219,2402190,Sovellettu matematiikka ja tilastotiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24199,Strategia ja tiedepolitiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409280,Strategian ja tiedepolitiikan yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240928,Strategian ja tiedepolitiikan yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240917,Strategiarahat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409170,Strategiarahat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240268,Structural and Chemical Biology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402680,Structural and Chemical Biology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240275,Structural enzymology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402750,Structural enzymology,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24000,240039,Suomen kieli,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240039,2400390,Suomen kieli,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400314,Suomen kieli,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240305,Suun terveyden tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403050,Suun terveyden tutkimusyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240559,Systeemitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240559,2405590,Systeemitekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403410,Syövän ja translationaalisen lääketieteen tutk,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240341,Syövän ja translationaalisen lääketieteen tutk,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24030,240341,Syövän ja translationaalisen lääketieteen tutkimyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240341,2403410,Syövän ja translationaalisen lääketieteen tutkimyksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405952,Sähkötekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240558,Säätötekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240558,2405580,Säätötekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24050,TEKNILLINEN TIEDEKUNTA,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24062,TIETO- JA SÄHKÖTEKNIIKAN TIEDEKUNTA,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2410,24062,TIETO- JA SÄHKÖTEKNIIKAN TIEDEKUNTA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240593,Tieto- ja sähkötekniikan tiedekunnan yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240593,2405930,Tieto- ja sähkötekniikan tiedekunnan yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240963,TK-lähipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409630,TK-lähipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409381,TKI kapasiteettipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405942,TST Fablab,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240595,TST Koulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405950,TST Koulutus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405933,TST Strateginen rahoitus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240594,TST Tiedekuntainfra,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405940,TST Tiedekuntainfra,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240593,2405932,TST Tietohallinto,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240593,2405933,TST Strateginen rahoitus,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240593,2405941,TST Työpaja,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240593,2405942,TST Fablab,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240594,TST Tiedekuntainfra,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240594,2405940,TST Tiedekuntainfra,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240594,2405943,Fablab Infrastruktuuri,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240595,TST Koulutus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240595,2405950,TST Koulutus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240595,2405951,Tietotekniikan ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240595,2405952,Sähkötekniikan ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240595,2405953,Tietojenkäsittelytietieteiden ala,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240589,Sovellettu ja laskennallinen matematiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240589,2405890,Sovellettu ja laskennallinen matematiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240208,"Empiirinen ohjelmistotuotanto ohjelmistoissa, järjestelmissä ja palveluissa",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240208,2402080,"Empiirinen ohjelmistotuotanto ohjelmistoissa, järjestelmissä ja palveluissa",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240596,Vakuuttavat verkko- ja mobiilipalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240596,2405960,Vakuuttavat verkko- ja mobiilipalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240597,Käyttöliittymät ja ihmiskeskeinen digitalisaatio,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240597,2405970,Käyttöliittymät ja ihmiskeskeinen digitalisaatio,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240582,Optoelektroniikka ja mittaustekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240582,2405820,Optoelektroniikka ja mittaustekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240583,Elektroniikan piirit ja järjestelmät,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240583,2405830,Elektroniikan piirit ja järjestelmät,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240586,Mikroelektroniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240586,2405860,Mikroelektroniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240584,Konenäkö ja signaalianalyysi,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240584,2405840,Konenäkö ja signaalianalyysi,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240585,Biosignaalien analyysi,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240585,2405850,Biosignaalien analyysi,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240599,Biomimetiikka ja älykkäät järjestelmät,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240599,2405990,Biomimetiikka ja älykkäät järjestelmät,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240611,Jokapaikan tietotekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240611,2406110,Jokapaikan tietotekniikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240587,CWC - Radioteknologiat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240587,2405870,CWC - Radioteknologiat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240612,CWC - Verkot ja järjestelmät,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240612,2406120,CWC - Verkot ja järjestelmät,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2420,Painoalat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2420,24064,Eudaimonia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24064,240640,Eudaimonia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240640,2406400,Eudaimonia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2420,24066,Biocenter,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24066,240681,Biocenter,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240681,2406810,Biocenter,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2420,24068,Infotech,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24068,240680,Infotech,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240680,2406800,Infotech,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2420,24077,Thule-instituutti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24077,240770,Thule-instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405941,TST Työpaja henkilöstökulut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405944,TST työpaja infra,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405934,TST yhteisten muu henkilöstö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405460,TTK tutkimuksen strateginen rahoitus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240546,TTK tutkimuksen strateginen rahoitus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409240,Talouden suunnittelu ja kehitys,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24194,Talous,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240943,2409433,Taloushallinto Kontinkangas,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240943,2409473,Taloushallinto keskitetyt talouspalvelut palvelupiste,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240924,Talouspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240924,2409240,Talouspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409241,Talouspalvelut yhteiset palvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24040,240403,Taloustieteen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240403,2404030,Taloustieteen yksikkö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240404,"Taloustieteen, laskentatoimen ja rahoituksen yksik",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2404040,"Taloustieteen, laskentatoimen ja rahoituksen yksik",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240500,Teknillinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405000,Teknillinen tiedekunta yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409491,Tellus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240949,Tellus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24020,240223,Teoreettinen fysiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240223,2402230,Teoreettinen fysiikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2403004,Terveystieteiden ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24077,Thule-instituutti,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240770,Thule-instituutti,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240770,2407700,Thule-instituutti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240770,2407701,Arktinen lääketiede,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240770,2407702,Uarctic toimisto,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2430,Alueyksiköt,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2430,24073,Kerttu Saalasti instituutti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240730,Kerttu Saalasti instituutti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240730,2407300,Kerttu Saalasti instituutti,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240731,KSI Aluekehittäminen,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240731,2407310,KSI Aluekehittäminen,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240732,KSI Tulevaisuuden tuotantoteknologiat (FMT),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240732,2407320,KSI Tulevaisuuden tuotantoteknologiat (FMT),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240733,KSI Mikroyrittäjyys (MikroY),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240733,2407330,KSI Mikroyrittäjyys (MikroY),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240734,KSI Maanalainen fysiikka (CUPP),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240734,2407340,KSI Maanalainen fysiikka (CUPP),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24073,240736,KSI Raahe,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240736,2407360,KSI Raahe,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2430,24076,Sodankylän geofysiikan observatorio,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24076,240760,Sodankylän geofysiikan observatorio,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240760,2407600,Sodankylän geofysiikan observatorio,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24076,240762,SGO Tutkimus- ja kehitystoiminta,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240762,2407620,SGO Tutkimus- ja kehitystoiminta,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2430,24080,Kajaanin yliopistokeskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24080,240800,Kajaanin yliopistokeskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240800,2408000,Kajaanin yliopistokeskus yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24080,240806,CEMIS-Oulu,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240806,2408060,CEMIS-Oulu,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240806,2408061,Analyyttinen kemia/bioanalytiikka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240806,2408062,Kuvantavat mittaukset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240806,2408063,Optinen spektroskopia,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24080,240807,AIKOPA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240807,2408071,AIKOPA,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2440,Erillisyksiköt,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2440,24065,UniOGS,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24065,240650,University of Oulu Graduate School,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240650,2406500,University of Oulu Graduate School,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2440,24081,Kirjasto,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24081,240810,Kirjasto,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408100,Kirjasto yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408101,Hankintapalvelu,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2410,Tiedekunnat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240541,Tiedekuntalaboratorio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405410,Tiedekuntalaboratorio,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240034,2400341,Tiedeviestinnän maisteriohjelma,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2400333,Tiedeviestinnän maisteriohjelma,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240593,Tieto- ja sähkötekniikan tiedekunnan yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405930,Tieto- ja sähkötekniikan tiedekunnan yhteiset,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408102,Tietoaineistopalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408103,Lainaus- ja neuvontapalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408104,Koulutus- ja tietopalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240810,2408150,Kirjaston hallintopalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2440,24086,Täydentävien opintojen keskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24086,240820,Kieli- ja viestintäkoulutus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240820,2408200,Kieli- ja viestintäkoulutus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24086,240840,Aikuiskoulutus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240840,2408400,Aikuiskoulutus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240840,2408406,Avoin yliopisto,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2445,Infrastruktuuri,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24067,Biocenter Core,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24067,240670,Biocenter Core,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240670,2406700,Biocenter Core,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24079,Mikroskopian ja nanoteknologian keskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24079,240790,Mikroskopian ja nanoteknologian keskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240790,2407900,Mikroskopian ja nanoteknologian keskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24083,Koe-eläinkeskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24083,240830,Koe-eläinkeskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240830,2408300,Koe-eläinkeskus,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24085,Oulangan tutkimusasema,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24085,240771,Oulangan tutkimusasema,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240771,2407710,Oulangan tutkimusasema,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24088,LEAF,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24088,240778,LEAF,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240778,2407780,LEAF,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2445,24089,Biodiversiteettiyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24089,240212,Biodiversiteettiyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240212,2402120,Biodiversiteettiyksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2450,Yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2450,24091,Yliopiston yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24091,240910,Yliopiston yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240910,2409100,Yliopiston yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24091,240911,Kiinteistöt,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409110,Kiinteistöt,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409115,Tilanvaraustilat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409125,Autopaikat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409130,Rakentaminen ja kunnossapito,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409135,Turvallisuus ja ympäristö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409140,Aulapalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409150,Postipalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409155,Kalusteasentajat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240911,2409160,Hankintapalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24091,240917,Strategiarahat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240917,2409170,Strategiarahat,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24091,240918,YO Tietohallinto,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240918,2409180,YO Lisenssit,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240918,2409181,YO Infrastruktuuri,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240918,2409182,YO Tietojärjestelmät,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24091,240901,YO Koulutuspalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240901,2409010,YO Koulutuspalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24,2460,Johto ja palvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,2460,24092,Johto ja palvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240920,YK-hyvitykset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240920,2409200,YK-hyvitykset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240900,Rehtoraatin yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240900,2409000,Rehtoraatin yksikkö,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240921,Johto ja palvelut - yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240921,2409210,Johto ja palvelut - yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240922,Henkilöstöpalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240922,2409220,Henkilöstöpalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240923,Koulutuspalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409230,Resurssipooli,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409232,Opintotuki (entinen työelämäpalvelut) 31.7.2016 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409236,Opetuksen tuki,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409237,Opiskelijavalinta,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409238,Opiskeluoikeuden ylläpito,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240923,2409239,Opiskelun tuki,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240924,Talouspalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240924,2409240,Talouspalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240925,Tila- ja turvallisuuspalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240925,2409250,Tila- ja turvallisuuspalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240925,2409251,Virastomestaripalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240927,"Viestintä, markkinointi ja yhteiskuntasuhteet",, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240927,2409270,"Viestintä, markkinointi ja yhteiskuntasuhteet",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240961,Tietohallinto lähipalvelutiimi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409610,Tietohallinto lähipalvelutiimi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240930,Tietohallintopalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409300,Tietohallintopalvelut yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405953,Tietojenkäsittelytietieteiden ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409360,Tietojärjestelmäpalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405951,Tietotekniikan ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409115,Tilanvaraustilat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240546,Tohtorikoulutettavat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240546,2405460,Tohtorikoulutettavat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240545,Tuotantotalouden ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405450,Tuotantotalouden ala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240570,Tuotantotalous,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405700,Tuotantotalous,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240531,Tuotantotekniikka 31.12.2015 saakka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409135,Turvallisuus ja ympäristö,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409252,Turvallisuuspalvelut,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240928,Tutkimuksen strategiset palvelut,, Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240928,2409280,Tutkimuksen strategiset palvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240930,Tietohallintopalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409300,Tietohallintopalvelut yhteiset,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409305,Asiakirjapalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409360,Tietojärjestelmäpalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409380,Infrapalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409385,Opetuksen ja tutkimuksen IT-tukipalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240930,2409390,Asiakaspalvelu,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240941,Palvelupisteet henkilöstöhallinto,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240941,2409471,Henkilöstöhallinto keskitetyt palvelut (KEPA),, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240942,Palvelupisteet koulutuspalvelut 31.7.2016 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409412,Koulutuspalvelut HKT 31.7.2016 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409422,Koulutuspalvelut LUTK 31.7.2016 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409432,Koulutuspalvelut Kontinkangas 31.7.2016 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240942,2409442,Koulutuspalvelut TTK 31.7.2016 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240943,Palvelupisteet talouspalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240943,2409433,Taloushallinto Kontinkangas,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240943,2409473,Taloushallinto keskitetyt talouspalvelut palvelupiste,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240948,Tutkimuksen tukipalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240948,2409480,Tutkimuksen tukipalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240949,Innovaatio- ja yrittäjyyspalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240949,2409245,Innovaatio- ja yrittäjyyspalvelut,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240720,Center for Health and Technology,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240720,2407200,Center for Health and Technology,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24092,240775,CEE Innovaatiokeskittymä,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240775,2407750,CEE Innovaatiokeskittymä,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240531,Tuotantotekniikka 31.12.2015 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240773,NorTech 31.12.2015 saakka,, -Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24062,240700,CIE 31.12.2015 saakka,, -Poliisiammattikorkeakoulu,Police University College,Polisyrkeshögskolan,02557,,,,http://isni.org/isni/0000000097577818,1602 -Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,,,http://isni.org/isni/0000000404184038,31 -Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2017,Opiskelijapalvelut ja opetuksen yhteiset,, -Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2040,TKI- ja palvelutoiminta,, -Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2099,AMK yhteiset palvelut,, -Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2100,Kulttuuri,, -Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2200,Liiketalous,, -Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2310,Hotelli- ja ravintola-ala,, -Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2500,Tekniikka,, -Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2400,Sosiaali- ja terveysala,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240948,Tutkimuksen tukipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409480,Tutkimuksen tukipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24195,Tutkimus- ja projektipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240538,Työpaja,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240538,2405380,Työpaja,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240224,Tähtitiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2402240,Tähtitiede,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24086,Täydentävien opintojen keskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2407702,Uarctic toimisto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24065,UniOGS,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240650,University of Oulu Graduate School,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2406500,University of Oulu Graduate School,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409271,"VMY, yliopiston ja OAMKin yhteiset palvelut",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240273,Vainio research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240273,2402730,Vainio research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240596,Vakuuttavat verkko- ja mobiilipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405960,Vakuuttavat verkko- ja mobiilipalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409820,Valtion pääomittamat varat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240982,Valtion pääomittamat varat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409830,Valtion pääomittamat varat 2,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240983,Valtion pääomittamat varat 2,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24097,Vapaat rahastot,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409810,Varainhankinta 2015-2017,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240981,Varainhankinta 2015-2017,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409800,Varainkeruuvarat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240980,Varainkeruuvarat,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24050,240552,Vesi- ja ympäristötekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240552,2405520,Vesi- ja ympäristötekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405520,"Vesi-, energia- ja ympäristötekniikka",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240552,"Vesi-, energia- ja ympäristötekniikka",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240962,Viestinnän lähipalvelutiimi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409620,Viestinnän lähipalvelutiimi,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24196,Viestintä,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240927,"Viestintä, markkinointi ja yhteiskuntasuhteet",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409270,"Viestintä, markkinointi ja yhteiskuntasuhteet",, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409251,Virastomestaripalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240274,Wei research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240274,2402740,Wei research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,24031,240275,Wierenga and Venkatesan research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,240275,2402750,Wierenga and Venkatesan research group,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240920,YK-hyvitykset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409200,YK-hyvitykset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409181,YO Infrastruktuuri,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240901,YO Koulutuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409010,YO Koulutuspalvelut,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409180,YO Lisenssit,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240918,YO Tietohallinto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409182,YO Tietojärjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2450,Yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240990,Yliopiston innovaatiokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409900,Yliopiston innovaatiokeskus,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,24091,Yliopiston yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240910,Yliopiston yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409100,Yliopiston yhteiset,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2409910,Yliopiston yleishallinto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240991,Yliopiston yleishallinto,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240555,Ympäristö- ja kemiantekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405550,Ympäristö- ja kemiantekniikka,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,240532,Älykkäät koneet ja järjestelmät,, +Oulun yliopisto,University of Oulu,Uleåborgs universitet,01904,,2405320,Älykkäät koneet ja järjestelmät,, +Oulun yliopistollisen sairaalan erityisvastuualue,Oulu University Hospital Catchment Area,,06794809,,,,, +Poliisiammattikorkeakoulu,Police University College,,02557,,,,, +Ruokavirasto,Finnish Food Authority,,430001,,,,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,,,http://isni.org/isni/0000000404184038,31 +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2099,AMK yhteiset palvelut,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2310,Hotelli- ja ravintola-ala,, Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2900,Kielikeskus,, -Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,,,http://isni.org/isni/0000000101657504,1748 -Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,TECH,Teknologia,, -Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,HYVO,Hyvinvointi,, -Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,LOME,Logistiikka ja meriteknologia,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2100,Kulttuuri,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2200,Liiketalous,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2017,Opiskelijapalvelut ja opetuksen yhteiset,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2400,Sosiaali- ja terveysala,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2040,TKI- ja palvelutoiminta,, +Saimaan ammattikorkeakoulu,Saimaa University of Applied Sciences,,02609,,2500,Tekniikka,, +Satakunnan ammattikorkeakoulu,Satakunta University of Applied Sciences,,02507,,,,, +Satakunnan ammattikorkeakoulu,Satakunta University of Applied Sciences,Satakunnan ammattikorkeakoulu,02507,,HYVO,Hyvinvointi ja terveys,, +Satakunnan ammattikorkeakoulu,Satakunta University of Applied Sciences,Satakunnan ammattikorkeakoulu,02507,,LOME,Logistiikka ja meriteknologia,, +Satakunnan ammattikorkeakoulu,Satakunta University of Applied Sciences,Satakunnan ammattikorkeakoulu,02507,,OPVA,Opetuksen vararehtorin toimisto,, Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,OPET,Opetuspalvelut,, -Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,PATA,Palveluliiketoiminta,, -Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,RETO,Rehtorin toimisto,, -Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,TAHA,Talous ja hallinto,, +Satakunnan ammattikorkeakoulu,Satakunta University of Applied Sciences,Satakunnan ammattikorkeakoulu,02507,,PATA,Palveluliiketoiminta,, +Satakunnan ammattikorkeakoulu,Satakunta University of Applied Sciences,Satakunnan ammattikorkeakoulu,02507,,RETO,Rehtorin toimisto,, Satakunnan ammattikorkeakoulu,SAMK Satakunta University of Applied Sciences,,02507,,TKI,TKI-palvelut,, -Savonia,University of Applied Sciences Savonia,,02537,,,,http://isni.org/isni/0000000404176521,1759 -Savonia,University of Applied Sciences Savonia,,02537,,100,LiTe vastuualueen yhteiset,, -Savonia,University of Applied Sciences Savonia,,02537,,110,Teknologia ja ympäristöala,, -Savonia,University of Applied Sciences Savonia,,02537,,300,Liiketalousala,, -Savonia,University of Applied Sciences Savonia,,02537,,350,Matkailu- ja ravitsemisala,, -Savonia,University of Applied Sciences Savonia,,02537,,400,Hyvin vastuualueen yhteiset,, -Savonia,University of Applied Sciences Savonia,,02537,,410,Sosiaali- ja terveysala,, -Savonia,University of Applied Sciences Savonia,,02537,,500,Kulttuuriala,, -Savonia,University of Applied Sciences Savonia,,02537,,600,Luonnonvara-ala,, -Savonia,University of Applied Sciences Savonia,,02537,,800,Savonia liiketoiminta,, -Savonia,University of Applied Sciences Savonia,,02537,,900,Hallinto- ja korkeakoulupalvelut,, -Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,,,,http://isni.org/isni/0000000104772049,U2VpbsOkam9lbiBhbW1hdHRpa29ya2Vha291bHU= -Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,ElMa,ElMa,SeAMK Elintarvike ja maatalous,, -Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,ElMa,MaMe,Luonnonvara (maa- & metsätalousalat),, +Satakunnan ammattikorkeakoulu,Satakunta University of Applied Sciences,Satakunnan ammattikorkeakoulu,02507,,TAHA,Talous ja hallinto,, +Satakunnan ammattikorkeakoulu,Satakunta University of Applied Sciences,Satakunnan ammattikorkeakoulu,02507,,TECH,Teknologia,, +Satakunnan ammattikorkeakoulu,Satakunta University of Applied Sciences,Satakunnan ammattikorkeakoulu,02507,,TUVA,Tutkimuksen vararehtorin toimisto,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,,02537,,,,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,900,Hallinto- ja korkeakoulupalvelut,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,400,Hyvin vastuualueen yhteiset,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,121,Jatkuva oppiminen,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,920,Kampukset ja toimitilat,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,131,Kansainvälisyys,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,500,Kulttuuriala,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,100,LiTe vastuualueen yhteiset,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,300,Liiketalousala,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,600,Luonnonvara-ala,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,350,Matkailu- ja ravitsemisala,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,902,Muut palvelut,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,901,Opiskelijaa palvelevat palvelut,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,840,Palveluliiketoiminta,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,800,Savonia liiketoiminta,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,990,Savonia yhteiset,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,410,Sosiaali- ja terveysala,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,830,TK-toiminta,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,110,Teknologia ja ympäristöala,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,111,Tutkintokoulutus,, +Savonia-ammattikorkeakoulu,Savonia University of Applied Sciences,Savonia-ammattikorkeakoulu,02537,,700,Yrityspalvelut,, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,,,,, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,Seinäjoen ammattikorkeakoulu,02472,,BiEli,Bio- ja elintarviketekniikka (tekniikan alat),, Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,ElMa,BiEli,Bio- ja elintarviketekniikka (tekniikka),, -Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,ElMa,Resto,Ravitsemisala (palveluala),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,Seinäjoen ammattikorkeakoulu,02472,,KiTi,Kirjasto ja tietopalvelu (yhteiskunnalliset alat),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,Seinäjoen ammattikorkeakoulu,02472,,Kult,Kulttuuri (taiteet ja kulttuuri),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,Seinäjoen ammattikorkeakoulu,02472,,Liike,"Liiketalous (kauppa, hallinto & oikeus)",, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,Seinäjoen ammattikorkeakoulu,02472,,MaMe,Luonnonvara (maa- & metsatalousalat),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,ElMa,MaMe,Luonnonvara (maa- & metsätalousalat),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,Seinäjoen ammattikorkeakoulu,02472,,Resto,Ravitsemisala (palveluala),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,ElMa,ElMa,SeAMK Elintarvike ja maatalous,, Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,LiKu,LiKu,SeAMK Liiketoiminta ja kulttuuri,, -Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,LiKu,Liike,"Liiketalous (kauppa, hallinto & oikeus)",, -Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,LiKu,Kult,Kulttuuri (taiteet ja kulttuuri),, -Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,LiKu,KiTi,Kirjasto ja tietopalvelu (yhteiskunnalliset alat),, -Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,SosTer,SosTer,SeAMK Sosiaali- ja terveysala (terveys & hyvinvointi),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,Seinäjoen ammattikorkeakoulu,02472,,SosTer,SeAMK Sosiaali- ja terveysala (terveys & hyvinvoin,, Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,Tekn,Tekn,SeAMK Tekniikka,, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,Seinäjoen ammattikorkeakoulu,02472,,Teka,SeAMK Tekniikka (tekniikan alat),, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,Seinäjoen ammattikorkeakoulu,02472,,Tsto,SeAMK Toimisto,, +Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,Seinäjoen ammattikorkeakoulu,02472,,Tite,Tietotekniikka (tietojenkasittely & tietoliikenne),, Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,Tekn,Tite,Tietotekniikka (tietojenkäsittely & tietoliikenne),, -Seinäjoen ammattikorkeakoulu,Seinäjoki University of Applied Sciences,,02472,Tsto,Tsto,SeAMK Toimisto,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,,,http://isni.org/isni/0000000111777737,SGFua2VuIFN2ZW5za2EgaGFuZGVsc2jDtmdza29sYW4= -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,1,Förvaltningsämbetet,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,2,Institutionen för finansiell ekonomi,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,3,Institutionen för företagsledning och organisation,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,4,Institutionen för redovisning och handelsrätt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,5,Institutionen för marknadsföring,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,6,Institutionen för nationalekonomi,, +Suomen Akatemia,Academy of Finland,,02458939,,,,, +Suomen Lääketieteen Säätiö,Finnish Medical Foundation,,02211063,,,,, +Suomen Pankki,Bank of Finland,,02022481,,,,, +Suomen ympäristökeskus,Finnish Environment Institute,,7020017,,,,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020180001,Administrative services,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020110001,Biodiversiteettikeskus,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020130001,Centre for sustainable consumption and production,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020100010,Climate change programme,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020190001,Communications,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020170001,Data and information centre,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020200001,Enheten for internationella ärenden,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020140001,Freshwater centre,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020120001,Havscentret,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020100001,Johdon tuki,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020100030,Kestävä kiertotalous,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020160001,Laboratoriecentret,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020150001,Miljöpolitikscentret,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020100040,Program om miljöinformation,, +Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,7020100020,Sustainable urbanisation programme,, +Svenska handelshögskolan,Hanken School of Economics,,01910,,,,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8355,Aarresaari-nätverket,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7421,Allmän adm. och personalärenden - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7422,Allmän adm. och personalärenden - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,8742,Allmän adm. och personalärenden / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7422,Allmän administration och personalärenden - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7421,Allmän administration och personalärenden -Helsi,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8742,Allmän administration och personalärenden / proj,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7456,Alumni - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7561,Arkadia,, Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,7,Biblioteket,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8321,Biblioteket,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,3211,Biblioteket - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,3215,Biblioteket - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8525,CCR - Centre for Corporate Responsibility,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4251,CCR - PRME och Green Office,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8531,CERS - Centre for Relationship Marketing and Servi,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8532,CERS - Centre for Relationship Marketing and Servi,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,8531,CERS - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7660,Capman,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7521,Casa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,8,8331,Centret för forskn.o.int.är./projekt,, Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,8,Centret för forskning och internationella ärenden,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,9,Datacentralen,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,10,Hanken Fortbildning,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,11,Centret för språk och affärskommunikation,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,19,Samfinansierade enheter,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,20,Fristående institutioner,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,7,3211,Biblioteket - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,20,3212,Tritonia,, Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,8,3311,Centret för forskning och internationella ärenden,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,8,3331,Forskningsservice,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,9,3411,Datacentralen - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,9,3412,Datacentralen - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,2,4111,Finansiell ekonomi - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,2,4112,Finansiell ekonomi - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,2,4121,Statistik - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,2,4122,Statistik - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4211,Företagsledning och organisation - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4212,Företagsledning och organisation - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4221,Entreprenörskap och företagsledning - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4222,Entreprenörskap och företagsledning - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4231,Informationsbehandling - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4232,Informationsbehandling - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,4311,Handelsrätt - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,4312,Handelsrätt - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,4321,Redovisning - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,4322,Redovisning - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,4411,Marknadsföring - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,4412,Marknadsföring - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,4421,Logistik och samhällsansvar,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,4511,Nationalekonomi - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,4512,Nationalekonomi - Vasa,, Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4705,Centret för språk,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4711,Svenska - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4712,Svenska -Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4721,Finska - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4722,Finska - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4731,Engelska - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4732,Engelska - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4741,Tyska - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4742,Tyska - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4751,Franska - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4761,Spanska - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4771,Ryska - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7311,Öppna universitetet - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7312,Öppna universitetet - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7411,Rektorsämbetet - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7421,Allmän adm. och personalärenden - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7422,Allmän adm. och personalärenden - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7431,Studiebyrån - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7432,Studiebyrån - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7451,Marknadsföring och information - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7452,Marknadsföring och information - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7453,Karriärtjänster - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7455,Karriärtjänster - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7456,Alumni - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7458,Partnerskap,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7459,Fundraising,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7461,Ekonomiavdelningen,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7472,Näringslivskoordinator - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,11,Centret för språk och affärskommunikation,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4701,Centret för språk och affärskommunikation - Hel,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4702,Centret för språk och affärskommunikation - Vas,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4705,"Centret för språk och affärskommunikation, admi",, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7620,Danske Capital,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,9,Datacentralen,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,3411,Datacentralen - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,3412,Datacentralen - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8341,Datacentralen / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7610,Donationskapital,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8523,EPCE - Erling-Persson Centre for Entrepreneurship,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7640,EQ,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7531,Economicum,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7461,Ekonomiavdelningen,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8746,Ekonomiavdelningen / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4731,Engelska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4732,Engelska - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4221,Entreprenörskap och företagsledning - Helsingfor,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,4221,Entreprenörskap och företagsledning - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4222,Entreprenörskap och företagsledning - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8427,Entreprenörskap och företagsledning - Vasa / pro,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8422,Entreprenörskap och företagsledning / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7630,Evli,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7670,Evli PE,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7511,Fastigheter och servicepersonal - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8751,Fastighetsförvaltning - Helsingfors / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8752,Fastighetsförvaltning - Vasa / projekt,, Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7491,Fastighetspersonal och infra - Helsingfors,, Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7492,Fastighetspersonal och infra - Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7511,Fastigheter och servicepersonal - Helsingfors,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7721,Helsingforsalliansen,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,10,8316,"MBA, Modulkostnader",, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,10,8317,MBA,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,8,8331,Centret för forskn.o.int.är./projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,19,8351,KATAJA,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,2,8411,Finansiell ekonomi / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,8412,Statistik / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7491,Fastighetsservice - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7492,Fastighetsservice - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4111,Finansiell ekonomi - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4112,Finansiell ekonomi - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8416,Finansiell ekonomi - Vasa / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8411,Finansiell ekonomi / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4721,Finska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4722,Finska - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,22,Forsknings- och universitetsservice,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,3331,Forskningsservice,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8333,Forskningsservice / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4751,Franska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4752,Franska - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,20,Fristående institutioner,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7459,Fundraising,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4211,Företagsledning och organisation - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4212,Företagsledning och organisation - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8426,Företagsledning och organisation - Vasa / projekt,, Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,8421,Företagsledning och organisation / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,8422,Entreprenörskap och företagsledning / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,3,8423,Informationsbehandling / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,8431,Handelsrätt / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,8432,Redovisning / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,8441,Marknadsföring / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,8442,Logistik och samhällsansvar /projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,8451,Nationalekonomi / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,8471,Samtliga språk / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,10,8511,Hanken fortbildning Vasa,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,8521,IPR University Center,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,8531,CERS - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8421,Företagsledning och organisation /projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,1,Förvaltningsämbetet,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8529,"GODESS - Gender, Organization, Diversity, Equality",, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8565,"HCCG - Hanken Centre for Accounting, Finance and G",, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8535,HUMLOG - The Humanitarian Logistics and Supply Cha,, Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,8535,HUMLOG Institute,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4311,Handelsrätt - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4312,Handelsrätt - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8436,Handelsrätt - Vasa / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8431,Handelsrätt / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7482,Hanken Business Lab Stugan,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8749,Hanken Business Lab Stugan / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7481,Hanken Business Lab Torget,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8748,Hanken Business Lab Torget / projekt,, Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,4,8565,Hanken Centre for Corporate Governance,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,8742,Allmän adm. och personalärenden / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,8743,Studiebyrån / projekt,, -Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7321,Samlad service,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,10,Hanken Fortbildning,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8511,Hanken fortbildning Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7721,Helsingforsalliansen,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8772,Helsingforsalliansen / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4151,Helsinki GSE / UKM,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8415,"Helsinki GSE / externa medel, projekt",, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7511,Huvudbyggnad - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7512,Huvudbyggnad - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8521,IPR University Center,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4231,Informationsbehandling - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4232,Informationsbehandling - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8428,Informationsbehandling - Vasa / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8423,Informationsbehandling / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,2,Institutionen för finansiell ekonomi,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,25,Institutionen för finansiell ekonomi och national,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,3,Institutionen för företagsledning och organisati,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,03,Institutionen för företagsledning och organisati,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,5,Institutionen för marknadsföring,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,05,Institutionen för marknadsföring,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,,6,Institutionen för nationalekonomi,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4,Institutionen för redovisning och handelsrätt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,04,Institutionen för redovisning och handelsrätt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7471,Institutionsadministration - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7472,Institutionsadministration - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7474,Institutionsadministration - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,3311,Internationella ärenden,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8331,Internationella ärenden / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,23,Internationella ärenden och externa relationer,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8351,KATAJA,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7453,Karriärtjänster - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7455,Karriärtjänster - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8744,Karriärtjänster / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4421,Logistik och samhällsansvar,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8447,Logistik och samhällsansvar - Vasa / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8442,Logistik och samhällsansvar / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,5,8442,Logistik och samhällsansvar /projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,10,8317,MBA,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,10,8316,"MBA, Modulkostnader",, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7650,Mandatum,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4411,Marknadsföring - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4412,Marknadsföring - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8446,Marknadsföring - Vasa / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8441,Marknadsföring / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7451,Marknadsföring och information - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7452,Marknadsföring och information - Vasa,, Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,8745,Marknadsföring och information / projekt,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,,,http://isni.org/isni/0000000417907610,1942 -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1260,KuvA/Johdon tuki,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1270,KuvA/Näyttelytoiminnan tuki,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1380,KuvA/Kv-avaukset,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1590,KuvA/Kirjasto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2201,TeaK/Ohjauksen koulutusohjelma,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2231,TeaK/Tanssin koulutusohjelma BA,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2822,TeaK/Opetuksen suunnitt ja keh,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2832,TeaK/Näyttämö ja tarpeisto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31160,SibA/Musiikkikasvatuksen aineryhmä,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3740,37410,Konserttivahtimestarit,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3750,SibA/Esitystekniikkapalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3801,SibA/Varadekaani I,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3830,SibA/Primo ohjelmapalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7100,Viestintäpalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7400,Opintopalvelut,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7451,Marknadsföring och kommunikation - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7452,Marknadsföring och kommunikation - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8745,Marknadsföring och kommunikation / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7811,Materiallagret - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7812,Materiallagret - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,4511,Nationalekonomi - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,4512,Nationalekonomi - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8418,Nationalekonomi - Vasa / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,6,8451,Nationalekonomi / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8413,Nationalekonomi / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4131,Nationalekonomi – Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4132,Nationalekonomi – Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7478,Näringslivskontakter - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8747,Näringslivskontakter - Helsingfors / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7479,Näringslivskontakter - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7472,Näringslivskoordinator - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7458,Partnerskap,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7590,"Placeringar, ej utlokaliserade",, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4321,Redovisning - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4322,Redovisning - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8437,Redovisning - Vasa / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8432,Redovisning / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,9234,Rektors projektmedel / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,24,Rektorsämbetet,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7411,Rektorsämbetet - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7412,Rektorsämbetet - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8741,Rektorsämbetet / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4771,Ryska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4772,Ryska - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,19,Samfinansierade enheter,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7321,Samlad service,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8472,Samtliga språk - Vasa / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8471,Samtliga språk / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4761,Spanska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4762,Spanska - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4121,Statistik - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4122,Statistik - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8417,Statistik - Vasa / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8412,Statistik / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,9237,Strategiska satsning / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7441,Studentrekrytering - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7431,Studiebyrån - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7432,Studiebyrån - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8743,Studiebyrån / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,21,Studier och antagning,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4711,Svenska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4712,Svenska - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,11,4712,Svenska -Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8360,Termins- och studieavgifter,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,20,3212,Tritonia,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,3212,Tritonia - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4741,Tyska - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,4742,Tyska - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8542,WCEFIR - Wallenberg Center for Financial Research,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8541,WCEFIR - Wallenberg Center for Financial Research,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,1100,Wahlbergs fond,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7311,Öppna universietet - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,8731,Öppna universitet / projekt,, +Svenska handelshögskolan,Hanken School of Economics,Svenska handelshögskolan,01910,,7312,Öppna universitetet - Vasa,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7311,Öppna universitetet - Helsingfors,, +Svenska handelshögskolan,Hanken School of Economics,Svenska Handelshögskolan,01910,1,7312,Öppna universitetet - Vasa,, +Svenska kulturfonden,,,23793568,,,,, +Svenska litteratursällskapet i Finland rf,Society of Swedish Literature in Finland,,02001381,,,,, +Syöpäsäätiö,Cancer Foundation,,02371657,,,,, +Säteilyturvakeskus,Radiation and Nuclear Safety Authority,,5550012,,,,, +Taideyliopisto,University of the Arts Helsinki,,10103,,,,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8400,Avoin kampus,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8420,Avoin yliopisto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7000,Henkilöstö- ja johdon tukipalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7310,IT/Sovellus- ja infrapalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7300,IT/Tietohallinto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7330,IT/Tukipalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8300,Jatkokoulutus ja tutkimus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41300,K/Henkilöstö- ja kehittämispalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,100,K/Johto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41101,K/Kuvanveisto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41200,K/Kuvataiteen tohtoriohjelma,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41103,K/Maalaustaide,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,411041,K/Nykytaiteen tutkimus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41302,K/Näyttelypalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,110,K/Opetus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41301,K/Opintopalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,130,K/Palvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41105,"K/Praxis, taiteen esittämiskäytännöt",, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41100,K/Taidegrafiikka,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,411040,K/Taidehistoria ja -teoria,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,411042,K/Taidepedagogiikka,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41303,K/Tekniset tukipalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41102,K/Tila-aikataide,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,120,K/Tohtorikoulutus ja tutkimus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41201,K/Tutkimustoiminta,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,41104,K/Yhteinen opetus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7610,Kallio-Kuninkala,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8210,Kielten opetus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7800,Kirjasto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,37820,Klassisen musiikin opintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3740,37410,Konserttivahtimestarit,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1390,KuvA/Esittämiskäytännöt ja tilallisuus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1250,KuvA/Henkilöstöpalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1260,KuvA/Johdon tuki,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1100,KuvA/Johto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1130,KuvA/Viestintä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1590,KuvA/Kirjasto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1320,KuvA/Kuvanveisto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1380,KuvA/Kv-avaukset,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1340,KuvA/Maalaustaide,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1500,KuvA/Nykytaiteen laboratorio,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1270,KuvA/Näyttelytoiminnan tuki,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1510,KuvA/Näyttelytoiminta,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1210,KuvA/Opintopalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1220,KuvA/Tekniset palvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1250,KuvA/Henkilöstöpalvelut,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1300,KuvA/Opiskelun tuki,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1310,KuvA/Taidegrafiikka,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1320,KuvA/Kuvanveisto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1430,KuvA/Taiteellinen tutkimus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1220,KuvA/Tekniset palvelut,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1330,KuvA/Tila-aikataide,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1340,KuvA/Maalaustaide,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1360,KuvA/Yhteisen opetuksen opetusalue,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1390,KuvA/Esittämiskäytännöt ja tilallisuus,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1410,KuvA/Tohtorikoulutusohjelma,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1430,KuvA/Taiteellinen tutkimus,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1500,KuvA/Nykytaiteen laboratorio,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1510,KuvA/Näyttelytoiminta,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2101,TeaK/Dekaani,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2200,TeaK/Näyttelijätyön koulutusoh,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2202,TeaK/Dramaturgian koulutusohj,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2203,TeaK/Live Art and Performance S,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2210,TeaK/MA in Ecology and Contemporary Performance,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2220,TeaK/Utbildingsprog. för skådespelarkonst,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2232,TeaK/Tanssijan maisteriohjelma,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2233,TeaK/Koreografin maisteriohj,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2241,TeaK/Valosuunnittelun koulutus,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2242,TeaK/Äänisuunnittelun koulutus,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2251,TeaK/Tanssiopettajan koulutus,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2252,TeaK/Teatteriopettajan koulutus,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2260,TeaK/Esittävien Taiteiden Tutk,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2270,TeaK/Yhteisen opetuksen keskus,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2811,TeaK/Hallintopalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2812,TeaK/Viestintä,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2821,TeaK/Opintohallinto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2823,TeaK/Kirjasto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2830,TeaK/Opetusteatteri,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2831,TeaK/Esitystuotanto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2833,TeaK/Puvusto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2834,TeaK/Valo ja ääni,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,3111,"SibA/Musiikkikasvatuksen, jazzin ja kansanmusiikin osasto",, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1130,KuvA/Viestintä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,1360,KuvA/Yhteisen opetuksen opetusalue,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,10,Kuvataideakatemia,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,37810,"Musiikkikasvatuksen, jazzin ja kansanmusiikin opintopalvelut",, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7690,N-talohanke,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8200,Opetus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7400,Opintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,37830,Orkesterikoulutuksen opintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8000,Rehtoraatti,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43204,S/Arts Management,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,330,S/Dekaanin alaisuudessa olevat palvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43108,S/DocMus-tohtorikoulu,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,434010,S/Esitystekniikka,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,432011,S/Global music,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43300,S/Henkilöstö- ja kehittämispalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,340,S/I Varadekaanin alaisuudessa olevat palvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,350,S/II Varadekaanin alaisuudessa,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43200,S/Jazzmusiikki,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43000,S/Johdon tuki,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,300,S/Johto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43100,S/Jouset ja kamarimusiikki,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,432010,S/Kamu,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43302,S/Kansainväliset asiat,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43201,S/Kansanmusiikki,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,431090,S/Kimu Kuopio,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,431091,S/Kimu Kuopio hallinto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43109,S/Kirkkomusiikki (Kuopio),, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43101,S/Kirkkomusiikki ja urut (Helsinki),, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,310,S/Klasu,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43110,S/Klasu johdon tuki,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,434004,S/Konserttivahtimestarit,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43402,S/Koulutuksen kehittäminen,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,431020,S/Laulumusiikki,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43102,S/Laulumusiikki,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,434002,S/Lavatiimi,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,320,S/MJK,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43207,S/MJK johdon tuki,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43205,S/MuTri-tohtorikoulu,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,432050,S/MuTri-tohtorikoulu1,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,432051,S/MuTri-tohtorikoulu2,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43103,S/Musiikin johtaminen,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43202,S/Musiikkikasvatus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43203,S/Musiikkiteknologia,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,431021,S/Ooppera,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43301,S/Opintopalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,434003,S/Orkesterisoitinten opintopalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43104,"S/Piano, harmonikka, kitara ja kantele",, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,431040,S/Pimu,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,434001,S/Primo,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43105,"S/Puhaltimet, lyömäsoittimset ja harppu",, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43206,S/Seinäjoki,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,434011,S/Soitinhuolto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,431041,S/Säestys,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43106,S/Sävellys ja musiikinteoria,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43401,S/Tekniset tuotantopalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43400,S/Tuottajapalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,434000,S/Tuottajat,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43500,S/Tutkimus- ja tohtorikoulutuspalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,43107,S/Vanha musiikki,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3800,SibA/Dekaani,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32270,SibA/DocMus-tohtorikoulu,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3750,SibA/Esitystekniikkapalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3705,SibA/Henkilöstö- ja johdon tukipalvelut,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31120,SibA/Jazzin aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32240,SibA/Jousten aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3817,SibA/Kansainväliset asiat,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31130,SibA/Kansanmusiikin aineryhmä,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31165,SibA/Taidehallinnon aineryhmä,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31190,SibA/Musiikkiteknologian aineryhmä,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31270,SibA/MuTri-tohtorikoulu,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3811,SibA/Kehittämiskeskus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3780,SibA/Keskitetyt opintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3600,SibA/Kirjasto,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32140,"SibA/Kirkkomusiikin ja urkujen aineryhmä, Hki",, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32150,"SibA/Kirkkomusiikin ja urkujen aineryhmä, Kuopio",, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32170,SibA/Laulun ja korrepetition aineryhmä,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32180,SibA/Sävellyksen ja musiikinteorian aineryhmä,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32210,SibA/Pianon aineryhmä (piano+kantele+kitara+harmonikka),, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,3222,SibA/Klassisen musiikin osasto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32230,"SibA/Orkesteri-, kamarimusiikki- ja kapellimestarikoulutuksen aineryhmä",, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32240,SibA/Jousten aineryhmä,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32250,SibA/Puhaltimien aineryhmä (lyömäsoittimet+harppu),, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32260,SibA/Vanhan musiikin aineryhmä,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32270,SibA/DocMus-tohtorikoulu,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,3251,SibA/Kuopion hallinto,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,3252,SibA/Kuopion kirjasto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32170,SibA/Laulun ja korrepetition aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31270,SibA/MuTri-tohtorikoulu,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31160,SibA/Musiikkikasvatuksen aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,3111,"SibA/Musiikkikasvatuksen, jazzin ja kansanmusiikin osasto",, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31190,SibA/Musiikkiteknologian aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32230,"SibA/Orkesteri-, kamarimusiikki- ja kapellimestarikoulutuksen aineryhmä",, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32210,SibA/Pianon aineryhmä (piano+kantele+kitara+harmonikka),, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3830,SibA/Primo ohjelmapalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32250,SibA/Puhaltimien aineryhmä (lyömäsoittimet+harppu),, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,3400,SibA/Seinäjoen toimipaikka,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3600,SibA/Kirjasto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3705,SibA/Henkilöstö- ja johdon tukipalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3710,SibA/Viestintäpalvelut,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3730,SibA/Soitinten huolto ja hankinnat,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32180,SibA/Sävellyksen ja musiikinteorian aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,31165,SibA/Taidehallinnon aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3814,SibA/Tohtoriohjelma,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3740,3740,SibA/Tuotantopalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3780,SibA/Keskitetyt opintopalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3111,37810,"Musiikkikasvatuksen, jazzin ja kansanmusiikin opintopalvelut",, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,37820,Klassisen musiikin opintopalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,37830,Orkesterikoulutuksen opintopalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3800,SibA/Dekaani,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,3222,32260,SibA/Vanhan musiikin aineryhmä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3801,SibA/Varadekaani I,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3802,SibA/Varadekaani II,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3811,SibA/Kehittämiskeskus,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3814,SibA/Tohtoriohjelma,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3817,SibA/Kansainväliset asiat,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3710,SibA/Viestintäpalvelut,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,3825,SibA/Yritysyhteistyö,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7000,Henkilöstö- ja johdon tukipalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7110,Varainhankinta,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,30,Sibelius-Akatemia,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42102,T/Dramaturgian koulutusohjelma,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,220,T/Esittävien taiteiden tutkimuskeskus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42300,T/Johdon tuki,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,200,T/Johto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42113,T/Kirjoittamisen maisteriohjelma,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42108,T/Koreografian maisteriohjelma,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42103,T/Live Art and Performance Studies,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42104,T/MA in Eco and Contemp.Per,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42100,T/Näyttelijäntaiteen koulutusohjelma,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,423040,T/Näyttämö ja tarpeisto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42101,T/Ohjauksen koulutusohjelma,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42301,T/Opetuksen koordinointipalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42302,T/Opetuksen suunnittelupalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,210,T/Opetus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,230,T/Palvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,423041,T/Puvusto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42107,T/Tanssijantaiteen maisteriohjelma,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42106,T/Tanssin koulutusohjelma BA,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42111,T/Tanssinopettajan maisteriohjelma,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42112,T/Teatteriopettajan maisteriohjelma,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42200,T/Tohtorikoulutus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42304,T/Tuotantotekniset palvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42303,T/Tuottajapalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42201,T/Tutkimus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42105,T/Utbildningsprog. I skådespelarkonst,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,423042,T/Valo ja ääni,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42109,T/Valosuunnittelun koulutus- ja maisteriohjelmat,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42114,T/Yhteisen opetuksen keskus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,42110,T/Äänisuunnittelun koulutus- ja maisteriohjelmat,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7200,Talouspalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7300,IT/Tietohallinto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7310,IT/Sovellus- ja infrapalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7330,IT/Tukipalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2101,TeaK/Dekaani,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2202,TeaK/Dramaturgian koulutusohj,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2260,TeaK/Esittävien Taiteiden Tutk,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2831,TeaK/Esitystuotanto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2811,TeaK/Hallintopalvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2823,TeaK/Kirjasto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2233,TeaK/Koreografin maisteriohj,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2203,TeaK/Live Art and Performance S,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2210,TeaK/MA in Ecology and Contemporary Performance,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2200,TeaK/Näyttelijätyön koulutusoh,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2832,TeaK/Näyttämö ja tarpeisto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2201,TeaK/Ohjauksen koulutusohjelma,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2822,TeaK/Opetuksen suunnitt ja keh,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2830,TeaK/Opetusteatteri,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2821,TeaK/Opintohallinto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2833,TeaK/Puvusto,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2232,TeaK/Tanssijan maisteriohjelma,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2231,TeaK/Tanssin koulutusohjelma BA,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2251,TeaK/Tanssiopettajan koulutus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2252,TeaK/Teatteriopettajan koulutus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2220,TeaK/Utbildingsprog. för skådespelarkonst,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2834,TeaK/Valo ja ääni,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2241,TeaK/Valosuunnittelun koulutus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2812,TeaK/Viestintä,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2270,TeaK/Yhteisen opetuksen keskus,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,2242,TeaK/Äänisuunnittelun koulutus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,20,Teatterikorkeakoulu,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7600,Toimitilapalvelut,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7610,Kallio-Kuninkala,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7690,N-talohanke,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7800,Kirjasto,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8000,Rehtoraatti,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8200,Opetus,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8210,Kielten opetus,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8300,Jatkokoulutus ja tutkimus,, -Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8400,Avoin kampus,, Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,8410,Täydennyskoulutus,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,,,,http://isni.org/isni/0000000103468395,36 -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311257,Media-alan koulutus,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H2,311003,Henkilöstöpalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311004,Talous- ja projektipalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H2,311007,Matkapalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311013,Laadunhallinta ja toiminnanohjaus,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311014,Floworks,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311017,Kehittämisyksikkö,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311021,Hallintopalvelut (ja tapahtumapalvelut),, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311022,Liikuntapalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311030,Hankintapalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311031,Viestintäpalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311040,Kiinteistöpalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311041,Tilapalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311050,Kirjasto- ja tietopalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311060,Tietohallinto,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311080,Opintopalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311090,Kansainväliset palvelut,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7110,Varainhankinta,, +Taideyliopisto,Uniarts Helsinki,Konstuniversitetet,10103,,7100,Viestintäpalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47203,Y/Avoimen kampuksen palvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,740,Y/Avoimen kampuksen yhteinen opetus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47401,Y/Avoin kampus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47403,Y/Erikoistumisopinnot,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,720,Y/HR-ja palvelujohtajan alaisuudessa olevat palvel,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47200,Y/Henkilöstö- ja kehittämispalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472000,Y/Henkilöstöpalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47300,Y/Historiafoorumi,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47211,Y/IT/Tietohallinto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472123,Y/Iltavahtimestarit,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,71,Y/Johto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472001,Y/Kehittämispalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47400,Y/Kielten opetus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47204,Y/Kirjasto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472040,Y/Kirjasto Siba,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472041,Y/Kirjasto TeaK/Kuva,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47221,Y/Kumppanuuspalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47230,Y/Lakipalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,723,Y/Lakipalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47201,Y/Opintopalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,72,Y/Palvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,730,Y/Research Hub,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472111,Y/Sovellus- ja infrapalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472102,Y/Sörnäisten kampus_ta,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472122,Y/Sörnäisten kampus_ti,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472202,Y/Sörnäisten kampus_vi,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47302,Y/Taidekasvatuksen tutkimuksen tk CERADA,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47301,Y/Taiteellisen tutkimuksen tk CfAR,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,721,Y/Talousjohtajan alaisuudessa olevat palvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47210,Y/Talouspalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472110,Y/Tietohallinto,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47212,Y/Tilapalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47310,Y/Tohtorikoulutus ja tutkimus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,731,Y/Tohtorikoulutus ja tutkimus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472112,Y/Tukipalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47202,Y/Tutkimuspalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472101,Y/Töölön kampus_ta,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472121,Y/Töölön kampus_ti,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472201,Y/Töölön kampus_vi,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,722,Y/Viestintä- ja kumppanuuspalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47220,Y/Viestintäpalvelut,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,47402,Y/Yhteinen opetus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,74,Y/Yhteinen opetus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,73,Y/Yhteinen tutkimus,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472100,Y/Yhteiset_ta,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472120,Y/Yhteiset_ti,, +Taideyliopisto,University of the Arts Helsinki,Konstuniversitetet,10103,,472200,Y/Yhteiset_vi,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,,,,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311402,Ajoneuvotekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311402,Ajoneuvotekniikan ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4104010,Ammatillinen opettajankoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4140000,Ammattikorkeakoulun johto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4140,Ammattikorkeakoulun johto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4146,Ammattikorkeakoulun yhteiset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311831,Ammattipedagoginen TKI,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4120010,Ammattipedagoginen TKI,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311831,Ammattipedagoginen TKI,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311021,Asiakirjahallinnon ja johdon tuki,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311414,Automaatioteknologia YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311414,Automaatioteknologia YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106030,Autotekniikan tutkinto-ohjelma,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311110,Avoin amk,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4131000,Avoin amk,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105040,Bachelor's Degree Programme in Environmental Engin,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102030,Bachelor's Degree Programme in International Busin,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4103030,Bachelor's Degree Programme in Media and Arts,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4107030,Bachelor's Degree Programme in Nursing,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311505,Bioanalytiikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311505,Bioanalytiikan ko / Bioanalyytikko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101030,Bioanalytiikko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101040,Bioanalytiikko Seinäjoki,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311408,Biotuote- ja prosessitekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311408,Biotuote- ja prosessitekniikan ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105030,Biotuotetekniikan tutkinto-ohjelma,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311410,DP in Energy and Environmental Engineering,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311301,DP in International Business ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311255,DP in Media and Arts,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311511,DP in Nursing,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106100,Dataosaaminen ja tekoäly YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311410,Degree Programme in Energy and Environmental Engineering,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102040,Degree Programme in IBM/MEL (MBA),, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311301,Degree Programme in International Business,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311255,Degree Programme in Media and Arts,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130000,EDU hallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4121030,EDUn projektit,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311254,Elokuvan ja television ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311524,"Ensihoitaja-, Kätilö- ja terveydenhoitajakoulutu",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4107040,"Ensihoitaja-, kätilö- ja terveydenhoitaja",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311504,Ensihoitajakoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4132000,Erikoistumiskoulutukset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4107050,Fiiliskeskus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311014,Floworks,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311445,Fysiikka,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311445,Fysiikka,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4104030,Fysiikka ja matematiikka,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101050,Fysioterapeutin tutkinto-ohjelma,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101060,Fysioterapiaklinikka,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311507,Fysioterapian ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311507,Fysioterapian ko / Fysioterapeutti,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4141030,Hallintopalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311021,Hallintopalvelut (ja tapahtumapalvelut),, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311030,Hankintapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311030,Hankintapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4141,Henkilöstö- ja viestintäpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000052,Henkilöstö- ja viestintäpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H2,Henkilöstöhallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H2,Henkilöstöhallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4141020,Henkilöstön kehittäminen,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H2,311003,Henkilöstöpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4141000,Henkilöstöpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311003,Henkilöstöpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101,Hyvinvointi ja terveysteknologia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000224,Hyvinvointi ja terveysteknologia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101010,"Hyvinvointi ja terveysteknologia laboratoriot, opetustilat ja -tarvikkeet",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101020,Hyvinvointi ja terveysteknologia varaukset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101000,Hyvinvointi ja terveyteknologia hallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101110,Hyvinvointiteknologia YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311514,Hyvinvointiteknologian ko / YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311514,Hyvinvointiteknologian ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4142030,Impact areas,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4146000,Investoinnit,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102090,Kansainvälinen myynti ja myynnin johtaminen YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311302,Kansainvälisen myynnin ja myynnin johtamisen koul,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311090,Kansainväliset palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144030,Kansainväliset palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311090,Kansainväliset palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H6,Kehittäminen,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144000,Kehittäminen,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H6,Kehittäminen,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311014,Kehittäminen (ent. Floworks),, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311017,Kehittämisyksikkö,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311017,Kehittämisyksikkö,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4104040,Kielet,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311112,Kielipalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311250,"Musiikin ko, muusikko, Musiikkipedagogi",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311112,Kielipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311040,Kiinteistöpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311040,Kiinteistöpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4145,Kirjasto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4145000,Kirjasto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311050,Kirjasto- ja tietopalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311050,Kirjasto- ja tietopalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311529,Kliinisen asiantuntijan koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311529,Kliinisen asiantuntijan koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311401,Konetekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311401,Konetekniikan ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106040,Konetekniikan tutkinto-ohjelma,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4150000,Konsernikirjaukset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H4,Korkeakoulupalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H4,Korkeakoulupalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144,Koulutuksen ja oppimisen palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000051,Koulutuksen ja oppimisen palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144010,Koulutuksen kehittämispalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144020,Koulutuksen tukipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311100,Koulutuksen tukipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4108,Koulutuksen yhteiset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H5,Koulutus ja TKI-toiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H5,Koulutus ja TKI-toiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130050,Koulutusvienti,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311252,Kuvataiteen ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311255,Degree Programme in Media and Arts,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311256,Mediatuottamisen ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311252,Kuvataiteen ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102050,Kykylaakso,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311599,Kätilökoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311013,Laadunhallinta ja toiminnanohjaus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4143010,Laadunhallinta ja toiminnanohjaus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311409,Laboratoriotekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311409,Laboratoriotekniikan ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105050,Laboratoriotekniikan tutkinto-ohjelma,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311300,Liiketalouden ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311300,Liiketalouden ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311303,Liiketalouden ko maakunnat,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102060,Liiketalous,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130080,Liiketoiminnan TKI-projektit,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130070,Liiketoiminnan kehittäminen,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130010,Liiketoiminnan koulutukset ja osaamiset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H3,Liiketoiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102,Liiketoiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130,Liiketoiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000221,Liiketoiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H3,Liiketoiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H52,Liiketoiminta ja palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H52,Liiketoiminta ja palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102000,Liiketoiminta ja palvelut hallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102010,"Liiketoiminta ja palvelut laboratoriot, opetustila",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102020,Liiketoiminta ja palvelut varaukset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4143,Liiketoimintapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4143000,Liiketoimintapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000049,Liiketoimintapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144040,Liikkuvuusprojektit,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311022,Liikuntapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144090,Liikuntapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311022,Liikuntapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4103060,MA in Screenwriting YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311921,MD in Information Technology,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311558,MD in International Business Management / in Educa,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311456,MD in Management and Economy in the International,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311422,MD in Risk Management and Circular Economy,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311259,MD in Screenwriting YAMK,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311259,MDP in Screenwriting YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4142010,Maakuntakorkeakoulu,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311456,Master's Degree Programme in Management and Economy in the International Forest Sector,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311921,Master´s Degree in Information Technology,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311444,Matematiikka,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311444,Matematiikka,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H2,311007,Matkapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4141010,Matkapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311007,Matkapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4103,"Media, musiikki ja taide",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000220,"Media, musiikki ja taide",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4103000,"Media, musiikki ja taide hallinto",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4103010,"Media, musiikki ja taide laboratoriot, opetustilat",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4103020,"Media, musiikki ja taide varaukset",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4103040,Media-ala,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311257,Media-alan koulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311257,Media-alan koulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4103070,"Mediatuottaminen, Emerging Media YAMK",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311256,Mediatuottamisen ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311256,Mediatuottamisen ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311450,Metsätalouden ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311450,Metsätalouden ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105060,Metsätalouden tutkinto-ohjelma,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311251,"Musiikin ko, Musiikkipedagogi",, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311260,"Musiikin ko, Musiikkipedagogi YAMK",, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311300,Liiketalouden ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311301,Degree Programme in International Business,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311302,Yrittäjyyden ko YAMK,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311309,Yrittäjyyden ja tiimijohtamisen ko / Proakatemia,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311350,Tietojenkäsittelyn ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311351,Tietojärjestelmäosaamisen ko YAMK,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311401,Konetekniikan ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311402,Ajoneuvotekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311260,"Musiikin ko, Musiikkipedagogi YAMK",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311250,"Musiikin ko, muusikko",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311250,"Musiikin ko, muusikko, Musiikkipedagogi",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4103080,Musiikin ylempi tutkinto-ohjelma,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4103050,"Musiikkipedagogi, muusikko",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311080,Opintopalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144080,Opintopalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311080,Opintopalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144060,Opiskelijarekrytointi,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000204,Oppimisen ja hyvinvoinnin palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144070,Oppimisen ja hyvinvoinnin tuki,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130020,Osaamisen myynti,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4121020,PEDA koulutushankkeet,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311558,Palvelu- ja projektiliiketoiminnan ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130090,Palvelujen katteeton myynti,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311559,Palveluliiketoiminnan johtamisen koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311559,Palveluliiketoiminnan johtamisen koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311551,Palveluliiketoiminnan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311551,Palveluliiketoiminnan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4104,Pedagogiset ratkaisut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000219,Pedagogiset ratkaisut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4104000,Pedagogiset ratkaisut hallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4104020,Pedagogiset ratkaisut varaukset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4152000,Perusrahoituksen kirjaukset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102070,Proakatemia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311310,Proakatemia YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102100,Proakatemia/Yrittäjyyden YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4150030,Rahastojen poistot,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105,Rakennettu ympäristö ja biotalous,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000222,Rakennettu ympäristö ja biotalous,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105000,Rakennettu ympäristö ja biotalous hallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105010,"Rakennettu ympäristö ja biotalous laboratoriot,",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105020,Rakennettu ympäristö ja biotalous varaukset,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311403,Rakennus- ja yhdyskuntatekniikan ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311404,Sähkö- ja automaatiotekniikan ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311405,Tieto- ja viestintätekniikan ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311406,"Talotekniikan ko , sähköinen talotekniikka, LVI-tekniikka",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311403,Rakennus- ja yhdyskuntatekniikan ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105080,Rakennus- ja yhdyskuntatekniikka,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311407,Rakennusalan työnjohdon ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311408,Biotuote- ja prosessitekniikan ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311409,Laboratoriotekniikan ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311410,Degree Programme in Energy and Environmental Engineering,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311411,Rakentamisen ja talotekniikan ko YAMK,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311413,Teknologiaosaamisen johtamisen ko YAMK,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311414,Automaatioteknologia YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311407,Rakennusalan työnjohdon ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105090,Rakennusalan työnjohdon tutkinto-ohjelma,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105070,Rakennusarkkitehdin tutkinto-ohjelma,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311423,Rakennusarkkitehti,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311444,Matematiikka,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311445,Fysiikka,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311450,Metsätalouden ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311456,Master's Degree Programme in Management and Economy in the International Forest Sector,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311502,Sairaanhoitajakoulutus,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311503,Sosionomikoulutus,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311504,Ensihoitajakoulutus,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311505,Bioanalytiikan ko / Bioanalyytikko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311507,Fysioterapian ko / Fysioterapeutti,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311423,Rakennusarkkitehti,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105120,Rakentaminen YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311424,Rakentaminen YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H53,Rakentaminen ja ympäristöteknologia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H53,Rakentaminen ja ympäristöteknologia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311411,Rakentamisen ja talotekniikan ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H1,Rehtorin toimisto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311000,Rehtorin toimisto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102080,Restonomi,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4102110,Restonomi YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105130,Risk Management & Circular Economy (YAMK),, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101080,Röntgenhoitaja,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101100,Röntgenhoitaja Seinäjoki,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311510,Röntgenhoitajakoulutus,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311514,Hyvinvointiteknologian ko / YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311510,Röntgenhoitajakoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4107060,Sairaanhoitaja,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311502,Sairaanhoitajakoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311502,Sairaanhoitajakoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311417,Software Engineering,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311516,Sosiaali- ja terveysalan johtamisen koulutus (YAMK),, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101120,Sosiaaliala YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101070,Sosionomi,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311503,Sosionomikoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311503,Sosionomikoulutus,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311517,Sosionomin koulutus YAMK,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311518,Terveyden edistämisen koulutus YAMK,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311524,Terveydenhoitajakoulutus,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311529,Kliinisen asiantuntijan koulutus YAMK,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311551,Palveluliiketoiminnan ko,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311558,Palvelu- ja projektiliiketoiminnan ko YAMK,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311559,Palveluliiketoiminnan johtamisen koulutus YAMK,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311599,Kätilökoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311517,Sosionomin koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4101090,Sote monimuunto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4147,Strategiarahoitus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4147000,Strategiarahoitus OKM,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4120020,Strateginen TK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4144050,Summer School,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311404,Sähkö- ja automaatiotekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311404,Sähkö- ja automaatiotekniikan ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106080,Sähkö- ja automaatiotekniikan tutkinto-ohjelma,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311600,TAMK Ammatillinen opettajankoulutus,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H6,311831,Ammattipedagoginen TKI,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311921,Master´s Degree in Information Technology,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311925,Työelämäpalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,,H1,Rehtorin toimisto,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H2,Henkilöstöhallinto,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H3,Liiketoiminta,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H4,Korkeakoulupalvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H5,Koulutus ja TKI-toiminta,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H55,Terveys- ja sosiaalipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311600,TAMK Ammatillinen opettajankoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311110,TAMK EDU,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4142,TKI ja maksullinen palvelutoiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000050,TKI ja maksullinen palvelutoiminta,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4142000,TKI ja maksullinen palvelutoiminta hallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000063,TKI-palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311800,TKI-palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4120000,TKI-palvelut,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H51,"Taide, musiikki ja media",, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H52,Liiketoiminta ja palvelut,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H53,Rakentaminen ja ympäristöteknologia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H51,"Taide, musiikki ja media",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H53,311406,"Talotekniikan ko , sähköinen talotekniikka, LVI-tekniikka",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311411,Talotekniikan ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311406,"Talotekniikan ko, LVI-tekniikka / sähköinen talo",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105100,Talotekniikan tutkinto-ohjelma,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4105110,Talotekniikka YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311004,Talous- ja projektipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311004,Talous- ja projektipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4143020,"Talous-, projekti- ja hankintapalvelut",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4147010,Tampere3,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4100,Tampereen ammattikorkeakoulu,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000014,Tampereen ammattikorkeakoulu,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4150010,Tekniset kirjaukset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4150,Tekniset kustannuspaikat,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106110,Teknologiajohtaminen koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311413,Teknologiaosaamisen johtamisen ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311413,Teknologiaosaamisen johtamisen ko YAMK,, Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H54,Teollisuusteknologia,, -Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H1,H6,Kehittäminen,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106,Teollisuusteknologia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000223,Teollisuusteknologia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H54,Teollisuusteknologia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106000,Teollisuusteknologia hallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106010,"Teollisuusteknologia laboratoriot, opetustilat ja",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106020,Teollisuusteknologia varaukset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311518,Terveyden edistämisen koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311518,Terveyden edistämisen koulutus YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4107070,Terveyden edistämisen ylempi tutkinto-ohjelma,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H55,311524,Terveydenhoitajakoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4107,Terveys,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,1010000225,Terveys,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4107000,Terveys hallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4107010,"Terveys laboratoriot, opetustilat ja -tarvikkeet",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4107020,Terveys varaukset,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H5,H55,Terveys- ja sosiaalipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,H55,Terveys- ja sosiaalipalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311516,Terveys- ja sosiaalipalvelut YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106070,TiTe Software Engineering,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H54,311405,Tieto- ja viestintätekniikan ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311405,Tieto- ja viestintätekniikan ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311060,Tietohallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4143040,Tietohallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4143050,Tietohallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311060,Tietohallinto,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106050,Tietojenkäsittely,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311350,Tietojenkäsittely ko ,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311350,Tietojenkäsittelyn ko,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106120,Tietojärjestelmäosaaminen YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H51,311351,Tietojärjestelmäosaamisen ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311351,Tietojärjestelmäosaamisen ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106060,Tietotekniikan tutkinto-ohjelma,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4143030,Tila- ja kiinteistöpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4143060,Tila- ja kiinteistöpalvelut TAU,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H3,311041,Tilapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311041,Tilapalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130030,Tuotteistetut palvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4120,"Tutkimus, kehitys ja innovaatiotoiminta",, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4108010,Tutkintoon johtava myytävä koulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311925,Työelämäpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130040,Työvoimakoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4131,Täydennyskoulutus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4121000,Ulkoiset TKI-projektit,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4121010,Ulkoiset pedagogiset TKI-projektit,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4121,Ulkoiset projektit,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H4,311031,Viestintäpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4141040,Viestintäpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311031,Viestintäpalvelut,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4130060,Vuokraus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311924,Y-Kampus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4142020,Y-Kampus,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4108000,Yhteiset opinnot,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311309,Yrittäjyyden ja tiimijohtamisen ko / Proakatemia,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,311309,Yrittäjyyden ja tiimijohtamisen ko / Proakatemia;,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,,02630,H52,311302,Yrittäjyyden ko YAMK,, +Tampereen ammattikorkeakoulu,Tampere University of Applied Sciences,Tampereen ammattikorkeakoulu,02630,,4106090,Älyteollisuus YAMK,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,,,,http://isni.org/isni/0000000093279856,1972 -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,220,Yleispalvelujen yhteiset,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,221,Strateginen johtaminen,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,223,Viestintä,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,224,Kumppanuudet,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,225,Talous ja toiminnan ohjaus,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,911,Arkkitehtuurin laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,811,Elektroniikan ja tietoliikennetekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,611,Fysiikan laitos,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,226,Henkilöstöpalvelut,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,227,Tietohallinto,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,228,Tilapalvelut,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,23,231,Oppimisen tuki,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,23,232,Tutkintopalvelut,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,23,233,Opintopalvelut,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,24,241,Tutkimuksen kehittämispalvelut,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,24,242,Tutkimuspalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,711,Hydrauliikan ja automatiikan laitos,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,24,243,Innovaatiopalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,612,Kemian ja biotekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,913,Kielikeskus,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,24,244,Kirjasto,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,25,251,Tampere3,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,715,Kone- ja tuotantotekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,224,Kumppanuudet,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,610,Luonnontieteiden tiedekunta,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,611,Fysiikan laitos,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,612,Kemian ja biotekniikan laitos,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,613,Matematiikan laitos,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,614,Optoelektroniikan tutkimuslaitos,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,710,Teknisten tieteiden tiedekunta,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,711,Hydrauliikan ja automatiikan laitos,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,712,Materiaaliopin laitos,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,713,Systeemitekniikan laitos,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,715,Kone- ja tuotantotekniikan laitos,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,810,Tieto- ja sähkötekniikan tiedekunta,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,811,Elektroniikan ja tietoliikennetekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,23,233,Opintopalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,23,231,Oppimisen tuki,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,61,614,Optoelektroniikan tutkimuslaitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,914,Porin laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,912,Rakennustekniikan laitos,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,812,Signaalinkäsittelyn laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,221,Strateginen johtaminen,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,713,Systeemitekniikan laitos,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,813,Sähkötekniikan laitos,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,814,Tietotekniikan laitos,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,910,Talouden ja rakentamisen tiedekunta,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,911,Arkkitehtuurin laitos,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,912,Rakennustekniikan laitos,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,913,Kielikeskus,, -Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,914,Porin laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,225,Talous ja toiminnan ohjaus,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,25,251,Tampere3,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,71,710,Teknisten tieteiden tiedekunta,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,915,Teollisuustalouden laitos,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,916,Tiedonhallinnan ja logistiikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,810,Tieto- ja sähkötekniikan tiedekunta,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,227,Tietohallinto,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,81,814,Tietotekniikan laitos,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,228,Tilapalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,24,241,Tutkimuksen kehittämispalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,24,242,Tutkimuspalvelut,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,23,232,Tutkintopalvelut,, Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,91,918,Täydennyskoulutuskeskus Edutech,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,223,Viestintä,, +Tampereen teknillinen yliopisto,Tampere University of Technology,Tammerfors tekniska universitet,01915,22,220,Yleispalvelujen yhteiset,, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,,,http://isni.org/isni/0000000123146254,37 +Tampereen yliopisto,Tampere University,,10122,,,,, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2501,BioMediTech,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8150,Digitalisaatio-kehittämishanke,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8220,Henkilöstön kehittäminen ja assistenttipalvelut,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8210,Henkilöstöpalvelut,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,1010,Informaatioteknologian ja viestinnän tiedekunta,, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2502,Informaatiotieteiden yksikkö,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6160,Innovaatiokulttuuri-kehittämishanke,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6150,Innovaatiopalvelut ja kumppanuudet,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,5160,Jatkuva oppiminen -kehittämishanke,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,2120,Johdon tuki ja hallinnon kehittäminen,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,1020,Johtamisen ja talouden tiedekunta,, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2503,Johtamiskorkeakoulu,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8140,Kampuskehitys-kehittämishanke,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,1030,Kasvatustieteiden ja kulttuurin tiedekunta,, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,02504,Kasvatustieteiden yksikkö,, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2505,"Kieli-, käännös- ja kirjallisuustieteiden yksikkö",, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2572,Kielikeskus,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,2010,Kielikeskus,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,5140,Kielikeskus,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2573,Kirjasto,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,5030,Kirjasto,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6140,Kirjasto,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,5020,Koulutus ja oppiminen,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,5120,Koulutus ja oppiminen,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2580,Laboratoriopalvelut,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6020,Laboratoriopalvelut,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6120,Laboratoriopalvelut,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,1040,Lääketieteen ja terveysteknologian tiedekunta,, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2506,Lääketieteen yksikkö,, -Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,02507,"Viestinnän, median ja teatterin yksikkö",, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8010,Osaaminen ja kulttuuri,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,1050,Rakennetun ympäristön tiedekunta,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8020,Talous- ja tilapalvelut,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8120,Talouspalvelut,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,1060,Tekniikan ja luonnontieteiden tiedekunta,, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2508,Terveystieteiden yksikkö,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6030,Tietoarkisto,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6130,Tietoarkisto,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,5010,Tietohallinto,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8130,Tietohallinto,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8110,Toiminnanohjaus ja analytiikka,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,4010,Toiminnanohjaus ja suunnittelu,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6190,Tutkijakollegium,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6180,Tutkijakoulu,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6010,Tutkimus ja innovaatiot,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6110,Tutkimuspalvelut,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,5150,Työelämäyhteydet ja jatkuva oppiminen,, +Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,02507,"Viestinnän, median ja teatterin yksikkö",, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8230,Viestintä ja markkinointi,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,8030,"Viestintä, brändi ja markkinointi",, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,02509,Yhteiskunta- ja kulttuuritieteiden yksikkö,, -Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2580,Laboratoriopalvelut,, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2571,Yhteiskuntatieteellinen tietoarkisto,, -Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2572,Kielikeskus,, -Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2573,Kirjasto,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,1070,Yhteiskuntatieteiden tiedekunta,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,3010,Yhteistyö ja kumppanuudet,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,6170,Yhteistyö ja kumppanuudet -kehittämishanke,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,2110,Yliopistokeskusten koordinaatio,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,1120,Yliopiston hallitus,, +Tampereen yliopisto,Tampere University,Tammerfors universitet,10122,,1110,Yliopiston johto,, Tampereen yliopisto,University of Tampere,Tammerfors universitet,01905,,2592,Yliopistopalvelut,, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,,,,http://isni.org/isni/0000000404747718,2131 -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9611,961111,Yhteiset palvelut,, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962011,"Liiketalous, ICT ja kemiantekniikka, yht.",, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962012,Liiketalous,, +Tampereen yliopistollisen sairaalan erityisvastuualue,Tampere University Hospital Catchment Area,,08265978,,,,, +Teknologian tutkimuskeskus VTT Oy,VTT Technical Research Centre of Finland Ltd,,26473754,,,,, +Terveyden ja hyvinvoinnin laitos,Finnish Institute for Health and Welfare,,5610017,,,,, +Tieteellisten seurain valtuuskunta,Federation of Finnish Learned Societies,,05247045,,,,, +Tilastokeskus,Statistics Finland,,02454911,,,,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,,,,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,965020,"Amk, Ensihoito, terveydenhoito ja kätilötyö",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,963013,"Amk, Esittävät taiteet",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961135,"Amk, HR-palvelut",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,962513,"Amk, ICT",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,962514,"Amk, Kemianteollisuus",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961142,"Amk, Kiinteistöpalvelut",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961133,"Amk, Kirjasto- ja tietopalvelut",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961121,"Amk, Koulutuksen kehittäminen",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961124,"Amk, Kumppanuus ja kehittäminen",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,965021,"Amk, Kuntoutus, suun terveydenhoito ja diagnostise",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,963014,"Amk, Kuvataide",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,962517,"Amk, Liiketoiminta ja palvelut",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,963015,"Amk, Media-ala",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961132,"Amk, Opiskelijapalvelut",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961136,"Amk, Oppimisympäristöpalvelut",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961125,"Amk, Projektitoimisto",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,962518,"Amk, Rakennusteollisuus",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,965022,"Amk, Sairaanhoito",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,965023,"Amk, Sosiaali- ja kasvatusala",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961123,"Amk, TKI-toiminta",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,963011,"Amk, Taideakatemia, yht.",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,963016,"Amk, Taideakatemia/Master School",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961141,"Amk, Talous ja toiminnanohjaus",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,962511,"Amk, Tekniikka ja liiketoiminta, yht.",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,962516,"Amk, Teknologiateollisuus",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,962515,"Amk, Teli/Master School",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,965024,"Amk, Terhy/Master School",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,965011,"Amk, Terveys ja hyvinvointi, yht.",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961122,"Amk, Työelämäpalvelut",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961134,"Amk, Viestintäpalvelut",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,961111,"Amk, Yleishallinto",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,Turun ammattikorkeakoulu,02509,,962512,"Amk, Yrittäjyys ja myynti",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963013,Esittävät taiteet,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964014,"Hankinnat, myynti ja logistiikka",, Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962013,ICT,, Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962014,Kemiantekniikka,, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962015,LIKe monimuotokoulutukset ja YAMK,, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963011,"Taideakatemia, yht.",, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963012,"Taideakatemia, projektit",, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963013,Esittävät taiteet,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964012,"Konetekniikka, meritekniikka ja muotoilu",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965013,Kuntoutus ja terveysalan erityisalueet,, Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963014,Kuvataide,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962015,LIKe monimuotokoulutukset ja YAMK,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962012,Liiketalous,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9620,962011,"Liiketalous, ICT ja kemiantekniikka, yht.",, Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963015,Media-ala,, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963016,Taideakatemia monimuotokoulutukset ja YAMK,, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964011,"Tekniikka, ympäristö ja talous, yht.",, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964012,"Konetekniikka, meritekniikka ja muotoilu",, Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964013,"Rakentaminen, ympäristö ja energia",, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964014,"Hankinnat, myynti ja logistiikka",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965014,Sosiaaliala,, Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964015,TYT monimuotokoulutukset ja YAMK,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963016,Taideakatemia monimuotokoulutukset ja YAMK,, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963012,"Taideakatemia, projektit",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9630,963011,"Taideakatemia, yht.",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9640,964011,"Tekniikka, ympäristö ja talous, yht.",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965015,Terhy monimuotokoulutukset ja YAMK,, Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965011,"Terveys ja hyvinvointi, yht.",, Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965012,Terveysala,, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965013,Kuntoutus ja terveysalan erityisalueet,, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965014,Sosiaaliala,, -Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9650,965015,Terhy monimuotokoulutukset ja YAMK,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,,,http://isni.org/isni/0000000121981512,41 -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601000,Johto ja yliopiston yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601004,Muutostuki,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601011,Kiinanmylly,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601012,Seili,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601100,Talouspalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601105,Reskontrapalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601106,Matkapalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601107,Projektipalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601200,UTUGS,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601201,Tutkijatohtoriohjelma,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601210,LLK:n yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601211,LLK Matematiikan ja tilastotieteen lts,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601212,LLK Biokemian laitos,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601213,LLK Kemian lts,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601214,LLK Biologian lts,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601215,LLK Fysiikan ja tähtitieteen lts,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601216,LLK Informaatioteknologian lts,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601217,LLK Maantieteen ja geologian lts,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601218,LLK Biolääketieteen lts,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601219,LLK Kliininen laitos,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601220,LLK Hoitotieteen lts,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601221,LLK Hammaslääketieteen lts,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601230,TIAS-tutkijakollegiumin yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601231,TIAS Humanistinen tdk,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601232,TIAS Yhteiskuntatieteellinen tdk,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601233,TIAS Kasvatustieteiden tdk,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601234,TIAS Oikeustieteellinen tdk,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601235,TIAS Kauppakorkeakoulu,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601240,Tutkimuksen toimialan yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601241,Tutkimuksen kehittäminen,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601242,Tutkimuspalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601250,SKY yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601251,Innovaatiopalvelut ja koulutusvienti,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601252,Kehittäminen ja suunnittelu,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601260,TCSI yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601261,TCSI International,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601300,Viestintä,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601400,Henkilöstöpalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601401,IT-palvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601405,Yleispalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601406,Palvelujohtajan yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601410,Koulutuksen toimialan yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601411,Kansainväliset palvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601412,Ohjauksen ja koulutuksen tukipalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601413,Opiskelija- ja hakijapalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601414,Liikuntapalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601415,Koulutuksen toimiala tiedekunnissa,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601416,Avoin yliopisto 1.8. alkaen,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601420,Kirjaston yhteiset palvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601421,Oppimisen palvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601422,Tutkimuksen palvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601423,Tietoaineistojen saatavuuspalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601424,Hankintapalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601430,Toimitilapalvelujen yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601431,Tilasuunnittelu,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601432,Tila- ja kuljetuspalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601433,Majoituspalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601434,Protopaja,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601435,Yliopiston yhteiset tilat,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601436,Turvallisuus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601510,Vyörytettävät kustannukset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601611,Drug Development and Diagnostics,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601621,Learning and Education,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601631,Bioimaging,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601641,Digital Futures,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601651,New Bioresources,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601900,Rahastot,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2601930,Tiedekuntien stipendirahasto,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602000,Humanistisen tiedekunnan yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602001,Humanistisen tiedekunnan hallintopalvelu,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602002,Baltic Sea Regions Studies,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602100,Kieli- ja käännöstieteiden laitoksen yht,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602101,Englannin kieli,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602102,Espanja,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602103,Fonetiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602104,Italia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602105,Klassiset kielet ja antiikin kulttuuri,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602106,Pohjoismaiset kielet,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602107,Ranska,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602108,Saksan kieli,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602109,Volgalaiskielten tutkimusyksikkö,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602110,Suomen kieli ja suom-ugrilainen kielent,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602111,Venäjän kieli,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602114,Lauseopin arkisto,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602200,"Historian, kultt ja tait tutk lts:n yht",, +Turun ammattikorkeakoulu,Turku University of Applied Sciences,,02509,9611,961111,Yhteiset palvelut,, +Turun yliopisto,University of Turku,,10089,,,,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609023,ARD kehittäminen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609021,ARD koulutus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609022,ARD tutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609020,ARD yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609011,AVO Humanistisen tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609012,AVO Kasvatustieteiden tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609017,AVO Kauppakorkeakoulun opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609015,AVO Lääketieteellisen tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609013,AVO Matemaattis-luonnon tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609018,AVO Muut koulutukset ja hankkeet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609016,AVO Oikeustieteellisen tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609014,AVO Yhteiskuntatieteellisen tdk opinnot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604304,Alakoulu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609022,Aluekehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607301,Anestesiologia,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2602201,Arkeologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602202,Folkloristiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602203,Kansatiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602204,Kotimainen kirjallisuus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602205,Kulttuurihistoria,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602206,Mediatutkimus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602207,Musiikkitiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602208,Sukupuolentutkimus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602209,Suomen historia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602210,Taidehistoria,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602211,Uskontotiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602212,Yleinen historia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602213,Yleinen kirjallisuustiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602214,Kulttuurituotannon ja maisemantutkim ko,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602215,Kulttuurientutkimuksen arkisto,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602216,Kalevala instituutti,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602217,Luova kirjoittaminen,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2602218,Museologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603000,Yhteisk tdk yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603001,Yhteisk tdk hallintopalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603100,Psykologian ja logopedian laitoksen yht,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603101,Filosofia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603102,Logopedia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603103,Psykologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603104,PSYKONET -verkosto,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603105,Kognitiivisen neurotieteen tutk yks (KNT,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603106,Oppimistutkimuksen keskus (OTUK),, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603107,KiVa Koulu,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603111,KiVa Koulu elinkeinotoiminta,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603200,"Filosofian, poliittisen historian ja valtio-opin lts yht",, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603201,Poliittinen historia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603202,Valtio-oppi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603203,Eduskuntatutkimuksen keskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603204,Itä-Aasian tutkimus- ja koulutusk (CEAS),, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603205,Yliopistojen Aasia-verkosto,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603206,The Public Choice Research Centre (PCRC),, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603207,John Morton -keskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603208,Filosofia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603300,Sosiaalitieteiden laitoksen yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603301,Sosiaalipolitiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603302,Sosiaalityö,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603303,Sosiologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603304,Taloussosiologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603305,Sosiaalivak huippuas jatkok ohj (TOPSOS),, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2603306,SOSNET-verkosto (sosiaalityö),, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604000,Kasv tdk yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604001,Kasv tdk tiedekuntapalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604002,Oppimistutkimuksen keskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604021,Et Koulutusvienti,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604100,Kasvatustieteiden laitos,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604200,Opettajankoulutuslaitoksen yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604201,OKL Turku,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604202,OKL Rauma,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604300,Turun normaalikoulun yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604301,Lukiokoulutus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604302,Perusopetus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604303,Opettajankoulutus ja kehitystehtävät,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604400,Rauman normaalikoulun yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604401,Perusopetus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2604402,Opettajankoulutus ja kehitystehtävät,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2605000,Oikeustiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2605100,Oikeustieteellisen liiketoiminta,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606000,Mat luonn tdk yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606001,Mat luonn tdk hallintopalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606010,TYYK:n yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606011,Lapin tutkimuslaitos Kevo,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606012,Saaristomeren tutkimuslaitos,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606013,Aerobiologian yksikkö,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606100,Matematiikan ja tilastotieteen lait yht,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606101,Matematiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606102,Sovellettu matematiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606103,Tilastotiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606200,Biokemian laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602219,Arkeologia ja Suomen historia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609021,Asiantuntijakehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606702,Avaruustutkimuslaboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609010,Avoimen yliopiston yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601416,Avoin yliopisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601225,Avoin yliopisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602002,Baltic Sea Regions Studies,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609260,BioCity Turku,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609260,BioCity Turku,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606017,Biodiversiteetti ja ympäristönäytepankki,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606401,Biodiversiteettitutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606010,Biodiversiteettiyksikön yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601631,Bioimaging,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2606201,Biokemia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606202,Biotekniikka / FM,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606200,Biokemian laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606400,Biologian laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607100,Biolääketieteen laitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609063,Biomedical and Environmental ICT,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607005,Biopankki,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607302,Biostatistiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609200,Biotekniikan keskuksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609201,Biotekniikan keskus,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2606203,Biotekniikka / DI,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606202,Biotekniikka / FM,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609030,Brahea kehittämispalvelujen elinkein yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609020,Brahea kehittämispalvelujen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609000,Brahea-keskuksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609000,Brahea-keskuksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609830,COE huippuyksikkö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609830,COE huippuyksikkö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609250,Cell Imaging,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609250,Cell Imaging,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608710,Centre for Collaborative Research (CCR),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601109,Controller-palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606001,Dekaani,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607303,Diagnostinen radiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601641,Digital Futures,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601611,Drug Development and Diagnostics Platfor,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603203,Eduskuntatutkimuksen keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606402,Ekologia ja evoluutiobiologia,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2606204,Elintarvikekemia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606205,Molekulaarinen kasvibiologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606206,IFDRC,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606300,Kemian laitoksen yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606301,Kemian perusopetus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606302,Materiaalikemia ja kemiallinen analyysi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606303,Orgaaninen kemia ja kemiallinen biologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606304,Laitekeskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606305,JBL-laboratorio,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606400,Biologian laitoksen yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606401,Biodiversiteettitutkimus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606402,Ekologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606403,Ympäristötiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606404,Genetiikka,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2606405,Eläinfysiologia,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2606406,Eläinmuseo,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606407,Kasvimuseo,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606408,Kasvitieteellinen puutarha,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606409,Evoluutiobiologian sovelluskeskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606700,Fysiikan ja tähtitieteen laitoksen yhtei,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606701,Wihurin fysiikantutkimuslaboratorio,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606702,Avaruustutkimuslaboratorio,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606703,Teoreettinen fysiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606704,Teollisuusfysiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606705,Tuorlan observatorio,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606706,Materiaalitutkimuksen laboratorio,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606707,Kvanttioptiikan laboratorio,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606800,Informaatioteknologian laitoksen yhteise,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606801,Tietoliikennetekniikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606802,Sulautettu elektroniikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606803,Tietojenkäsittelytiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606804,Ohjelmistotekniikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606805,Bioinformatiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606806,Vuorovaikutussuunnittelu,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606900,Maantieteen ja geologian laitoksen yht.,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606901,Maantiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2606902,Geologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607000,Lääket tdk yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607001,Lääket tdk hallintopalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607002,Lääket tdk täydennyskoulutus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607003,MediCity,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607004,Sydäntutkimuskeskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607005,Biopankki,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607010,Turun lapsi- ja nuorisotutkimuskeskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607020,Funktionaalisten elint. kehittämiskeskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607030,Koe-eläinkeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609062,Embedded and Mixed-Reality Systems,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602101,Englannin kieli,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608311,Entre,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608310,Entren yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602102,Espanja,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609034,Et ARD kehittäminen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609032,Et ARD koulutus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609033,Et ARD tutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609030,Et ARD yhteiset,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607040,Et Aistila,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607100,Biolääketieteen laitoksen yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607101,Solubiologia ja anatomia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609033,Et Aluekehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609032,Et Asiantuntijakehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609031,Et Avoin yliopisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603111,Et KiVa Koulu elinkeinotoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609035,Et Kongressitoimisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601270,Et Koulutusviennin yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604021,Et Koulutusvienti,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601278,Et Koulutusvienti Erilliset laitokset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601271,Et Koulutusvienti Hum. tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601273,Et Koulutusvienti Kasv. tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601275,Et Koulutusvienti Luonn. ja tekn. tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601276,Et Koulutusvienti Lääk. tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601274,Et Koulutusvienti Oik. tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601277,Et Koulutusvienti TuKKK,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601272,Et Koulutusvienti Yht. tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609053,Et Mkk Kotka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609055,Et Mkk Merifoorumi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609055,Et Mkk Merifoorumi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609054,Et Mkk Meriklusteriohjelma,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609052,Et Mkk T&K-hankkeet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609052,Et Mkk T&K-hankkeet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609051,Et Mkk koulutukset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609050,Et Mkk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609050,Et Mkk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2605100,Et Oikeustieteellisen liiketoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608610,Et TSE Exe,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609034,Et Yliopistokehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609036,Et innovaatio- ja yrityskehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606409,Evoluutiobiologian sovelluskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609211,FMSC Bioinformatiikka,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607102,"Farmakologia, lääkekehitys ja lääkehoito",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603200,"Filosof, pol hist ja valtio-opin lts yht",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603101,Filosofia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603208,Filosofia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602221,Fokaus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602202,Folkloristiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602103,Fonetiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609210,Funktionaalisen genomiikan keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607020,Funktionaalisten elint. kehittämiskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607320,Fysiatria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606700,Fysiikan ja tähtitieteen laitoksen yhtei,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607103,Fysiologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607104,Lääketieteellinen biokemia ja genetiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607105,Lääketietee mikrobiologia ja immunologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607107,Patologia ja oikeuslääketiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607108,Virusoppi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607160,Oikeuslääketieteen maksupalveluyksikkö,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607300,Kliinisen laitoksen yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607301,Anestesiologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607302,Biostatistiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607303,Diagnostinen radiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606404,Fysiologia ja genetiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606902,Geologia,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607304,Geriatria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602200,HKT-laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607500,Hammaslääketieteen laitos yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601424,Hankintapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601108,Hankintapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601400,Henkilöstöpalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607400,Hoitotieteen laitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602001,Humanistisen tiedekunnan hallintopalvelu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602000,Humanistisen tiedekunnan yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601407,Hyvinvointipalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606206,IFDRC,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603023,INVEST Lippulaiva Lastenpsykiatria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603022,INVEST Lippulaiva Psykologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603021,INVEST Lippulaiva Sosiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601401,IT-palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601288,IT-palvelut,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607305,Iho- ja sukupuolitautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607051,InFLAMES Lippulaiva tutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607050,InFLAMES Lippulaiva yhteiset,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607306,Infektiotautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609025,Innovaatio- ja yrityskehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601251,Innovaatiopalvelut ja koulutusvienti,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601287,"Innovaatiot,yrittäjyys ja koulutusvienti",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602104,Italia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603204,Itä-Aasian tutkimus- ja koulutusk (CEAS),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606305,JBL-laboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603207,John Morton -keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608301,Johtaminen ja organisointi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608300,Johtaminen ja yrittäjyys yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601000,Johto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602216,Kalevala instituutti,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601411,Kansainväliset palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601224,Kansainväliset palvelut,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607307,Kansanterveystiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602203,Kansatiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604001,Kasv tdk tiedekuntapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604000,Kasv tdk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604100,Kasvatustieteiden laitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606407,Kasvimuseo,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606408,Kasvitieteellinen puutarha,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601252,Kehittäminen ja suunnittelu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601280,Kehittämispalvelut yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606300,Kemian laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606301,Kemian perusopetus,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607308,Keuhkosairausoppi ja kliin. allergologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603107,KiVa Koulu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602100,Kieli- ja käännöstieteiden laitoksen yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606805,Kieli- ja puheteknologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609400,Kieli- ja viestintäopintojen keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609400,Kielikeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608220,Kielten ja liikeviestinnän yksikkö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601011,Kiinanmylly,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602222,Kirjallisuustieteet ja kirjoittaminen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601420,Kirjasto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601700,Kirjasto,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607309,Kirurgia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607310,Ortopedia ja traumatologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602105,Klassiset kielet ja antiikin kulttuuri,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607322,Kliininen fysiologia,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607311,Kliininen kemia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607312,"Korva-, nenä-, ja kurkkutautioppi",, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607313,Lastentautioppi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607314,Neurologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607315,Kliininen syöpätautioppi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607316,Psykiatria,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607317,Silmätautioppi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607318,Sisätautioppi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607319,Synnytys- ja naistentautien oppi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607320,Fysiatria,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2607321,Kliininen neurofysiologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607322,Kliininen fysiologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607323,Lääketieteen etiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607324,Palliatiivinen lääketiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607325,Lastenneurologia,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607326,Lastenpsykiatria,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607327,Työterveyshuolto,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607328,Yleislääketiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607330,Turun lapsi- ja nuorisotutkimuskeskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607400,Hoitotieteen laitos,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607500,Hammaslääketieteen laitos yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2607511,TCBC,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608000,TuKKK:n yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608001,TuKKK:n hallintopalvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608002,TuKKK:n viestintä,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607315,Kliininen syöpätautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607300,Kliinisen laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607314,Kliiniset neurotieteet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607030,Koe-eläinkeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603105,Kognitiivisen neurotieteen tutk yks (KNT,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606022,Konetekniikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609024,Kongressitoimisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607312,"Korva-, nenä-, ja kurkkutautioppi",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602204,Kotimainen kirjallisuus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601418,Koto/Koulutuksen tuki,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601227,Koto/Koulutuksen tuki,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601419,Koto/Opiskelijavalinta ja hakijapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601228,Koto/Opiskelijavalinta ja hakijapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601417,Koto/Opiskelun tuki,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601226,Koto/Opiskelun tuki,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601415,Koulutuksen toimiala tiedekunnissa,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601410,Koulutuksen toimialan yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601223,Koulutuksen toimialan yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609900,Koulutussosiologian tutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603309,Koulutussosiologian tutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602215,Kulttuurientutkimuksen arkisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602205,Kulttuurihistoria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602214,Kulttuurituotannon ja maisemantutkim ko,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608202,Kv. liiketoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606707,Kvanttioptiikan laboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601212,LLK Biokemian laitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601214,LLK Biologian lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601218,LLK Biolääketieteen lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601215,LLK Fysiikan ja tähtitieteen lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601221,LLK Hammaslääketieteen lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601220,LLK Hoitotieteen lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601216,LLK Informaatioteknologian lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601213,LLK Kemian lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601219,LLK Kliininen laitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601217,LLK Maantieteen ja geologian lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601211,LLK Matematiikan ja tilastotieteen lts,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601222,LLK-tutkijatohtoriohjelma,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601210,LLK:n yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606304,Laitekeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601285,Lakiasiat,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606011,Lapin tutkimuslaitos Kevo,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2608100,Laskentatoimen ja rahoituksen laitos yht,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2608101,Laskentatoimi ja rahoitus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608102,Yritysjuridiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608103,Taloustieteen kvantitatiiviset menetelmä,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607325,Lastenneurologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607326,Lastenpsykiatria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604203,Lastentarhanopettajakoulutus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607313,Lastentautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602114,Lauseopin arkisto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601621,Learning and Education,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601414,Liikuntapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603102,Logopedia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604301,Lukiokoulutus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606000,Luonnontieteiden ja tekniikan tdk yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602217,Luova kirjoittaminen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601405,Lähipalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607001,Lääket tdk hallintopalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607002,Lääket tdk täydennyskoulutus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607000,Lääket tdk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607105,Lääketietee mikrobiologia ja immunologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607104,Lääketieteellinen biokemia ja genetiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607323,Lääketieteen etiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606901,Maantiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606900,Maantieteen ja geologian laitoksen yht.,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601433,Majoituspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601133,Majoituspalvelut,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2608200,Markk ja kv liiketoim laitos yhteiset,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2608201,Markkinointi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608202,Kv. liiketoiminta,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608203,Toimitusketjujen johtaminen,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608204,Talousmaantiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606100,Matematiikan ja tilastotieteen lait yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606101,Matematiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606302,Materiaalikemia ja kemiallinen analyysi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606021,Materiaalitekniikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606706,Materiaalitutkimuksen laboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601106,Matkapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607003,MediCity,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602223,"Median, musiikin ja taiteen tutkimus",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602206,Mediatutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609061,Microelectronics,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606807,Mikroelektroniikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609210,Mikrosirukeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609043,Mkk Kotka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609045,Mkk Merifoorumi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609045,Mkk Merifoorumi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609044,Mkk Meriklusteriohjelma,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609042,Mkk Yhteisrahoitteiset T&K-hankkeet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609042,Mkk Yhteisrahoitteiset T&K-hankkeet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609041,Mkk koulutustoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609040,Mkk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609040,Mkk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606205,Molekulaarinen kasvibiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602218,Museologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602207,Musiikkitiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601004,Muutostuki,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601651,New Bioresources,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604202,OKL Rauma,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604201,OKL Turku,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601412,Ohjauksen ja koulutuksen tukipalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606804,Ohjelmistotekniikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607160,Oikeuslääketieteen maksupalveluyksikkö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2605000,Oikeustiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604303,Opettajankoulutus ja kehitystehtävät,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604402,Opettajankoulutus ja kehitystehtävät,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604200,Opettajankoulutuslaitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601413,Opiskelija- ja hakijapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601421,Oppimisen palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604002,Oppimistutkimuksen keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603106,Oppimistutkimuksen keskus (OTUK),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606303,Orgaaninen kemia ja kemiallinen biologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607310,Ortopedia ja traumatologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609810,PET Perustoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609810,PET Perustoiminta,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609820,PET Tutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609820,PET Tutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603104,PSYKONET -verkosto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607324,Palliatiivinen lääketiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601406,Palvelujohtajan yhteiset,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2608210,Pan-Eurooppa Instituutti,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608220,Kielten ja liikeviestinnän yksikkö,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608300,Johtamisen ja yrittäjyyden laitos yhteis,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608301,Johtaminen ja organisointi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608303,Tietojärjestelmätiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608310,Entren yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608311,Entre,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608312,Yrittäjyys,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608320,"Turun työtieteiden keskus, TCLS",, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608400,Taloustiede,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608610,TSE Exe,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608710,Centre for Collaborative Research (CCR),, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608800,Porin tutkintokoulutus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607107,Patologia ja oikeuslääketiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604302,Perusopetus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604401,Perusopetus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602106,Pohjoismaiset kielet,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603201,Poliittinen historia,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2608802,Porin hankerahoitus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608800,Porin tutkintokoulutus,, Turun yliopisto,University of Turku,Åbo universitet,10089,,2608804,Porin yksikön yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608805,Porin täydennyskoulutus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,2608900,Tulevaisuuden tutkimuskeskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609000,Brahea-keskuksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601671,Profilaatio 4,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601681,Profilaatio 5,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601107,Projektipalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609240,Proteomics,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609240,Proteomics,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601434,Protopaja,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601134,Protopaja,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607316,Psykiatria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603103,Psykologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603100,Psykologian ja logopedian laitoksen yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601900,Rahastot,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602107,Ranska,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604400,Rauman normaalikoulun yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601105,Reskontrapalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609901,Rosa-laboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601012,Ruissalo,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601250,SKY yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603306,SOSNET-verkosto (sosiaalityö),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606012,Saaristomeren tutkimuslaitos,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602108,Saksan kieli,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601661,Sea and Maritime Studies,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607317,Silmätautioppi,, Turun yliopisto,University of Turku,Åbo universitet,10089,,02609001,Sisäiset palvelut,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609010,Avoimen yliopiston yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609011,AVO Humanistisen tdk opinnot,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609012,AVO Kasvatustieteiden tdk opinnot,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609013,AVO Matemaattis-luonnon tdk opinnot,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609014,AVO Yhteiskuntatieteellisen tdk opinnot,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609015,AVO Lääketieteellisen tdk opinnot,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609016,AVO Oikeustieteellisen tdk opinnot,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609017,AVO Kauppakorkeakoulun opinnot,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609018,AVO Muut koulutukset ja hankkeet,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609020,Brahea kehittämispalvelujen yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609021,Asiantuntijakehitys,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609022,Aluekehitys,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609023,Yliopistokehitys,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609024,Kongressitoimisto,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609025,Innovaatio- ja yrityskehitys,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609030,Brahea kehittämispalvelujen elinkein yht,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609031,Et Avoin yliopisto,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609032,Et Asiantuntijakehitys,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609033,Et Aluekehitys,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609034,Et Yliopistokehitys,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609035,Et Kongressitoimisto,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609036,Et innovaatio- ja yrityskehitys,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609040,Mkk yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609041,Mkk koulutustoiminta,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609042,Mkk Yhteisrahoitteiset T&K-hankkeet,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609043,Mkk Kotka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609044,Mkk Meriklusteriohjelma,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609045,Mkk Merifoorumi,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609050,Et Mkk yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609051,Et Mkk koulutukset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609052,Et Mkk T&K-hankkeet,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609053,Et Mkk Kotka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609054,Et Mkk Meriklusteriohjelma,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609055,Et Mkk Merifoorumi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607318,Sisätautioppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607101,Solubiologia ja anatomia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603301,Sosiaalipolitiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603300,Sosiaalitieteiden laitoksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603302,Sosiaalityö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603305,Sosiaalivak huippuas jatkok ohj (TOPSOS),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603303,Sosiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607007,Sote-akatemia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606102,Sovellettu matematiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606013,Soveltavan ympäristötutkimuksen laborato,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601284,Strateginen ohjaus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602208,Sukupuolentutkimus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606802,Sulautettu elektroniikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609700,Suomen ESO-keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609700,Suomen ESO-keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602209,Suomen historia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602110,Suomen kieli ja suom-ugrilainen kielent,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607004,Sydäntutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607319,Synnytys- ja naistentautien oppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607006,Syöpätutkimuslaboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607511,TCBC,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601261,TCSI International,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601260,TCSI yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601231,TIAS Humanistinen tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601233,TIAS Kasvatustieteiden tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601235,TIAS Kauppakorkeakoulu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601234,TIAS Oikeustieteellinen tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601232,TIAS Yhteiskuntatieteellinen tdk,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601230,TIAS-tutkijakollegiumin yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601236,TIAS-tutkijatohtoriohjelma,, Turun yliopisto,University of Turku,Åbo universitet,10089,,02609060,TRC yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609061,Microelectronics,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609062,Embedded and Mixed-Reality Systems,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609063,Biomedical and Environmental ICT,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609200,Biotekniikan keskuksen yhteiset,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609201,Biotekniikan keskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609210,Mikrosirukeskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609211,FMSC Bioinformatiikka,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609240,Proteomics,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609250,Cell Imaging,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609260,BioCity Turku,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609400,Kielikeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608805,TSE Porin yksikön kehittäminen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602210,Taidehistoria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608204,Talousmaantiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601100,Talouspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603304,Taloussosiologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608400,Taloustiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608103,Taloustieteen kvantitatiiviset menetelmä,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606020,Tekniikan laajennuksen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606704,Teollisuus fysiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606703,Teoreettinen fysiikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606808,Terveysteknologia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603206,The Public Choice Research Centre (PCRC),, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601930,Tiedekuntien stipendirahasto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601423,Tietoaineistojen saatavuuspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606803,Tietojenkäsittelytiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608303,Tietojärjestelmätiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606801,Tietoliikennetekniikka,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601432,Tila- ja kuljetuspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601132,Tila- ja kuljetuspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606103,Tilastotiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601431,Tilasuunnittelu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601131,Tilasuunnittelu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601430,Toimitilapalvelujen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601130,Toimitilapalvelujen yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608203,Toimitusketjujen johtaminen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608001,TuKKK:n hallintopalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608002,TuKKK:n viestintä,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608000,TuKKK:n yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606800,Tulevaisuuden teknologioiden laitoks yht,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608900,Tulevaisuuden tutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606019,Tuorla ja Luma-keskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606705,Tuorlan observatorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606104,Turku Complex Systems Institute Cern,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609201,Turun biotiedekeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2609200,Turun biotiedekeskus yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607010,Turun lapsi- ja nuorisotutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607330,Turun lapsi- ja nuorisotutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604300,Turun normaalikoulun yhteiset,, Turun yliopisto,University of Turku,Åbo universitet,10089,,02609500,Turun tietotekniikan tutkimuskeskus TUCS,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609700,Suomen ESO-keskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609810,PET Perustoiminta,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609820,PET Tutkimus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609830,COE huippuyksikkö,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609900,Koulutussosiologian tutkimuskeskus,, -Turun yliopisto,University of Turku,Åbo universitet,10089,,02609901,Rosa-laboratorio,, -Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,,,http://isni.org/isni/0000000406476659,2355 -Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,10,Tekniikan yksikkö,, -Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,20,Liiketalouden yksikkö,, -Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,30,Sosiaali- ja terveysalan yksikkö,, -Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,40,MUOVA,, -Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,50,Hallinto ja tukipalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608320,"Turun työtieteiden keskus, TCLS",, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601436,Turvallisuus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601283,Tutkijanura,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601200,Tutkijanurapalvelut yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601201,Tutkijatohtoriohjelma,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601241,Tutkimuksen kehittäminen,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601422,Tutkimuksen palvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601240,Tutkimuksen toimialan yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601282,Tutkimusedellytykset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601242,Tutkimuspalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601281,Tutkimusrahoitus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601435,Tyhjät tilat,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607327,Työterveyshuolto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601202,UTUGS,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602211,Uskontotiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602224,Uudet avaukset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603202,Valtio-oppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602111,Venäjän kieli,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601300,Viestintä,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607108,Virusoppi,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602109,Volgalaiskielten tutkimusyksikkö,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606806,Vuorovaikutusmuotoilu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601510,Vyörytettävät kustannukset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607008,Väestötutkimuskeskus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606701,Wihurin fysiikantutkimuslaboratorio,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601013,Yhteiset tilat ml. tyhjät ja opetustilat,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603001,Yhteisk tdk hallintopalvelut,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603000,Yhteisk tdk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603020,Yhteisk tdk yhteiset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602212,Yleinen historia,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602220,Yleinen historia ja kulttuurihistoria,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2602213,Yleinen kirjallisuustiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2607328,Yleislääketiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2603205,Yliopistojen Aasia-verkosto,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,02609023,Yliopistokehitys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2601286,Yliopiston vaikuttavuus,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2604305,Yläkoulu,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606018,Ympäristömuutokset,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2606403,Ympäristötiede,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608312,Yrittäjyys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608305,Yrittäjyys,, +Turun yliopisto,University of Turku,Åbo universitet,10089,,2608102,Yritysjuridiikka,, +Turun yliopistollisen keskussairaalan erityisvastuualue,Turku University Hospital Catchment Area,,08282559,,,,, +Työterveyslaitos,Finnish Institute of Occupational Health,,02202669,,,,, +Ulkopoliittinen instituutti,The Finnish Institute of International Affairs,,1120017,,,,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,,,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,Vaasan ammattikorkeakoulu,02627,,50,Hallinto ja tukipalvelut,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,Vaasan ammattikorkeakoulu,02627,,20,Liiketalouden yksikkö,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,Vaasan ammattikorkeakoulu,02627,,40,MUOVA,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,Vaasan ammattikorkeakoulu,02627,,30,Sosiaali- ja terveysalan yksikkö,, Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,,02627,,60,TKI-yksikkö,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,,,http://isni.org/isni/0000000106722619,2363 -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710050,Nykysuomi ja kääntäminen,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710100,Pohjoismaiset kielet,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710150,Englannin kieli,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710200,Ranskan kieli,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710250,Venäjän kieli,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710300,Saksan kieli ja kirjallisuus,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710350,Viestintätieteet,, +Vaasan ammattikorkeakoulu,Vaasa University of Applied Sciences,Vaasan ammattikorkeakoulu,02627,,10,Tekniikan yksikkö,, +Vaasan yliopisto,University of Vaasa,,01913,,,,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2701400,Aluetiede,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710400,Aluetiede,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710450,Sosiologia,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710500,Hallintotiede/julkisjohtaminen,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708330,"Aulapalvelut, tilat ja turvallisuus",, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760550,"Aulapalvelut, tilat ja turvallisuus",, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704820,Automaatiotekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730050,Automaatiotekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740050,Avoin yliopisto,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,270500,Digital Economy,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2705000,Digital Economy,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740150,Energia ja aluekehittäminen,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704810,Energiatekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730100,Energiatekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710150,Englannin kieli,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2701410,Filosofia,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710550,Filosofia,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710600,Julkisoikeus,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710650,Sosiaali- ja terveyshallintotiede,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710700,Kielipalvelut,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710900,Filosofinen tdk yht.,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730150,Fysiikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710500,Hallintotiede/julkisjohtaminen,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708110,Hallitus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760050,Hallitus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2701500,Henkilöstöjohtaminen,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708310,Henkilöstön ja ylimmän johdon palvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760300,Henkilöstön palvelut ja ylimmän johdon palvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,270600,Innolab,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2706000,Innolab,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708130,Johdon asiantuntijatuki,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760150,Johdon asiantuntijatuki,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740100,Johtaminen ja organisaatioiden kehittäminen,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720050,Johtaminen ja organisaatiot,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2701420,Julkisjohtaminen,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2701430,Julkisoikeus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710600,Julkisoikeus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760500,Kansainväliset asiat,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2702500,Kansanvälinen liiketoiminta,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720900,Kauppatieteell. tdk yht.,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,270700,Kielikeskus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2707000,Kielikeskus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710700,Kielipalvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2703500,Laskentatoimi,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720100,Laskentatoimi ja rahoitus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,274000,Levón-instituutti,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740900,Levón-instituutti yhteiset,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708350,"Lähipalvelut ja johdon palvelut, Fabriikki",, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760750,"Lähipalvelut ja johdon palvelut, Fabriikki",, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708360,"Lähipalvelut ja johdon palvelut, Tervahovi",, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760800,"Lähipalvelut ja johdon palvelut, Tervahovi",, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2702510,Markkinointi,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720150,Markkinointi,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720200,Taloustiede,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720250,Talousoikeus,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720900,Kauppatieteell. tdk yht.,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730050,Automaatiotekniikka,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730100,Energiatekniikka,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730150,Fysiikka,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730200,Sähkötekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704830,Matemaattiset tieteet,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730250,Matematiikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710050,Nykysuomi ja kääntäminen,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708210,Opiskelun ja opetuksen palvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760450,Opiskelun ja opetuksen palvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710100,Pohjoismaiset kielet,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2703510,Rahoitus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710200,Ranskan kieli,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708120,Rehtori,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760100,Rehtori,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704850,SC-Research,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2735050,SC-Research,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710300,Saksan kieli ja kirjallisuus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2703000,School of Accounting & Finance,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,270300,School of Accounting and Finance,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,270100,School of Management,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2701000,School of Management,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,270200,School of Marketing and Communication,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2702000,School of Marketing and Communication,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,270400,School of Technology and Innovations,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704000,School of Technology and Innovations,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2701440,Sosiaali- ja terveyshallintotiede,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710650,Sosiaali- ja terveyshallintotiede,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710450,Sosiologia,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2701510,Strateginen johtaminen,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704800,Sähkötekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730200,Sähkötekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730700,TB Laboratoriot,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708320,Talouden ja hankehallinnon palvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760350,Talouden ja hankehallinnon palvelut,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730300,Talousmatematiikka,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730350,Tilastotiede,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2703520,Talousoikeus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720250,Talousoikeus,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2703400,Taloustiede,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2720200,Taloustiede,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704860,Technobotnia,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730900,Teknillinen tdk. yhteiset,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708340,Tietohallintopalvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760650,Tietohallintopalvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704720,Tietoliikennetekniikka,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730400,Tietoliikennetekniikka,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704700,Tietotekniikka kaupp,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730450,Tietotekniikka kauppat.,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704710,Tietotekniikka tekn,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730500,Tietotekniikka tekn.,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760700,Tietotekniikkapalvelut,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730350,Tilastotiede,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760400,Toiminta- taloussuunnittelu,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,275000,Tritonia,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2750050,Tritonia,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730600,Tuotantotalous,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704500,Tuotantotalous kaupp,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2704840,Tuotantotalous tekn,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730660,Tuotantotalous tekn,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730700,TB Laboratoriot,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2730900,Teknillinen tdk. yhteiset,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2735050,SC-Research,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708220,Tutkimuksen palvelut ja tutkijakoulu,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760200,Tutkimuksen palvelut ja tutkijakoulu,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2790100,Vaasan korkeakoulukonsortio,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2780000,Vaasan yliopiston rahasto,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,278000,Vaasan yliopiston rahastot,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,273900,Vebic,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2739150,Vebic julkinen rahoitus,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2739050,Vebic omarahoitus,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2739100,Vebic taloudellinen toiminta,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2739150,Vebic julkinen rahoitus,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740050,Avoin yliopisto,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740100,Johtaminen ja organisaatioiden kehittäminen,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740150,Energia ja ympäristö,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2740900,Levón-instituutin yhteiset,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2750050,Tritonia,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760050,Hallitus,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760100,Rehtori,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760150,Johdon asiantuntijatuki,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760200,Tutkimuksen palvelut ja tutkijakoulu,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,270900,Vebic-alusta,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2709000,Vebic-alusta,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710250,Venäjän kieli,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2708140,Viestintä ja kumppanuudet,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760250,Viestintä ja kumppanuudet,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760300,Henkilöstön palvelut ja ylimmän johdon palvelut,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760350,Talouden ja hankehallinnon palvelut,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760400,Toiminta- taloussuunnittelu,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760450,Opiskelun ja opetuksen palvelut,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760500,Kansainväliset asiat,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760550,"Aulapalvelut, tilat ja turvallisuus",, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760650,Tietohallintopalvelut,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760700,Tietotekniikkapalvelut,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760750,"Lähipalvelut ja johdon palvelut, Fabriikki",, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2760800,"Lähipalvelut ja johdon palvelut, Tervahovi",, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2780000,Vaasan yo sijoitustoiminta,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2702400,Viestintätieteet,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2710350,Viestintätieteet,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2780050,Viljo Syreniuksen rahasto,, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,279000,Yliopiston yhteiset,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2790050,Yliopiston yhteiset,, -Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2790100,Vaasan korkeakoulukonsortio,, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,,,http://isni.org/isni/0000000404001027,163 -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,11,"HV, Pedagogik",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,111,"HV, Hälsa och välfärd (utom idrott)",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,112,"HV, Idrott",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,212,"EA, Turism",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,25,"EA, Tradenom",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,27,"EA, IT",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,38,"EM, Teknik",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,42,"KK, Media och kultur",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,51,"Övriga, Pedagogik",, +Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,270800,Yliopistopalvelut,, +Valtion taloudellinen tutkimuskeskus,VATT Institute for Economic Research,,3060016,,,,, +Väestörekisterikeskus,Population Register Center,,02454372,,,,, +Väylävirasto,Finnish Transport Infrastructure Agency,,10105471,,,,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,,02535,,,,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,111,Alla utbildningar på hälsa och välfärd föruto,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,611,Alla utbildningar på institutionen för vård,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,27,IT-utbildningen,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,112,Idrottsutbildningen,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,2,Institutionen för ekonomi och affärsanalys,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,3,Institutionen för energi- och materialteknologi,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,1,Institutionen för hälsa och välfärd,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,4,Institutionen för kultur och media,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,6,Institutionen för vård,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,42,Medie- och kulturutbildningen,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,21,Pedagogik på institutionen för ekonomi och affä,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,31,Pedagogik på institutionen för energi- och mater,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,11,Pedagogik på institutionen för hälsa och välfÃ,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,41,Pedagogik på institutionen för kultur och media,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,61,Pedagogik på institutionen för vård,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,38,Teknikutbildningen,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,25,Tradenomutbildningen,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,212,Turismutbildningen,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,9,Övriga enheter,, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,95,"Övriga, ekonomi, administration och juridik",, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,93,"Övriga, humanistiska området",, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,911,"Övriga, hälsa och välfärdsområdet",, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,97,"Övriga, informations- och kommunikationsteknik",, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,92,"Övriga, konst- och kulturområdet",, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,91,"Övriga, pedagogiska området",, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,912,"Övriga, service och tjänster",, +Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,98,"Övriga, teknikområdet",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,55,"Övriga, Ekonomi, administration och juridik",, Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,511,"Övriga, HV (utom idrott)",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,512,"Övriga, Service och tjänster",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,52,"Övriga, Konst- och kultur",, Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,53,"Övriga, Humaniora",, -Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,55,"Övriga, Ekonomi, administration och juridik",, Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,57,"Övriga, IT",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,52,"Övriga, Konst- och kultur",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,51,"Övriga, Pedagogik",, +Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,512,"Övriga, Service och tjänster",, Yrkeshögskolan Arcada,,Yrkeshögskolan Arcada,02535,,58,"Övriga, Teknik",, -Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,,,http://isni.org/isni/0000000406476587,2865 +Yrkeshögskolan Novia,Novia University of Applied Sciences,,10066,,,,, +Yrkeshögskolan Novia,Novia University of Applied Sciences,Yrkeshögskolan Novia,10066,,AD,Administration,, +Yrkeshögskolan Novia,Novia University of Applied Sciences,Yrkeshögskolan Novia,10066,,JA,Enheten Jakobstad;Enheten Jakobstad;,, +Yrkeshögskolan Novia,Novia University of Applied Sciences,Yrkeshögskolan Novia,10066,,VA,Enheten Vasa;Enheten Vasa;,, +Yrkeshögskolan Novia,Novia University of Applied Sciences,Yrkeshögskolan Novia,10066,,Ã…R,Enheten ÅboRaseborg;Enheten ÅboRaseborg;,, Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,FoU,Forskning och utveckling,, -Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,TS,Turism och Samhälle,, -Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,AD,Administration,, -Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,FE,Företagsekonomi,, +Yrkeshögskolan Novia,Novia University of Applied Sciences,Yrkeshögskolan Novia,10066,,BE,Institutionen för bioekonomi,, +Yrkeshögskolan Novia,Novia University of Applied Sciences,Yrkeshögskolan Novia,10066,,FE,Institutionen för företagsekonomi,, +Yrkeshögskolan Novia,Novia University of Applied Sciences,Yrkeshögskolan Novia,10066,,HV,Institutionen för hälsa och välfärd,, +Yrkeshögskolan Novia,Novia University of Applied Sciences,Yrkeshögskolan Novia,10066,,KK,Institutionen för konst och kultur,, +Yrkeshögskolan Novia,Novia University of Applied Sciences,Yrkeshögskolan Novia,10066,,TS,Institutionen för teknik och sjöfart,, Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,KU,Kultur,, Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,NB,Naturbruk,, Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,NV,Naturvetenskap,, Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,SF,Sjöfart,, Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,TE,Teknik,, Yrkeshögskolan Novia,,Yrkeshögskolan Novia,10066,,VS,Vård och sociala,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,,,http://isni.org/isni/0000000121652639,w4VibyBBa2FkZW1p -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801000,FHPT gemensamma kostnader,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801010,FHPT universitetsservice,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801100,"Kultur, historia och filosofi",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801110,Språk,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801111,Logopedi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801120,Teologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801130,Psykologi,, +Åbo Akademi,Åbo Akademi University,,01903,,,,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,22,Allmän pedagogik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,111,Analytisk kemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,118,Anläggningsteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,23,Barnpedagogik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,90,Biokemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804300,Biokemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,98,Bioteknikcentrum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,99,Bioteknikcentrum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804400,Bioteknikcentrum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280440,Bioteknikcentrum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804360,Biovet gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,95,Biovetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280430,Biovetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,92,Cellbiologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804311,Cellbiologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2805000,Centret för Livslångt Lärande,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280500,Centret för Livslångt Lärande,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2805010,Centret för Språk och Kommunikation,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,124,Centret för livslångt lärande,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,125,Centret för livslångt lärande (CLL),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,128,Centret för språk och kommunikation,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,129,Centret för språk och kommunikation (CSK),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,130,Datacentralen,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,131,Datacentralen,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,102,Datateknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804500,Datateknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,103,Datavetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804510,Datavetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,26,De matematisk-naturvetenskapliga ämnenas och idrottens didaktik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,65,Demografi och landsbygdsforskning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,19,Dnätverk Art History,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,123,Dnätverk Chemical Engineering,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804343,Dnätverk Functional Marine Biodiversity,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,106,Dnätverk IT and Mathematics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,96,Dnätverk Informational and Structural Biology,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,85,Dnätverk Material Research,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,97,Dnätverk Molecular Biosciences,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,43,Dnätverk Old Testament studies,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,48,Dnätverk Psychology and Logopedics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,70,Dnätverk Realizing Human Rights,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,57,Dnätverk School of Business and Economics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,42,Dogmatik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801142,Doktorandnätverk AAPL,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801140,Doktorandnätverk Art History,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804141,Doktorandnätverk Chemical Engineering,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804070,Doktorandnätverk Chemical Engineering,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803143,Doktorandnätverk Citizens and Democracy,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801146,Doktorandnätverk FHPT 2018-2021,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801147,Doktorandnätverk FHPT 2020-2023,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802141,Doktorandnätverk FPV 2018-2021,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802142,Doktorandnätverk FPV 2020-2023,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803144,Doktorandnätverk FSE 2018-2021,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803145,Doktorandnätverk FSE 2020-2023,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804540,Doktorandnätverk IT & Mathematics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804340,Doktorandnätverk Inform. and Struct. Bio,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804140,Doktorandnätverk Material Research,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804060,Doktorandnätverk Material Research,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801144,Doktorandnätverk Minority Research,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804341,Doktorandnätverk Molecular Biosciences,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801141,Doktorandnätverk Old Testament Studies,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801142,Doktorandnätverk AAPL (Abo Akademi Psychology and Logopedics),, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801143,"Gamla forskarskolan, FHPT",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801144,Doktorandnätverket Minority Research,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801145,Doktorandnätverket The Age of Sweden,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803140,Doktorandnätverk Realizing Human R.,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803141,Doktorandnätverk School of Business Economics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801145,Doktorandnätverk The Age of Sweden,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,142,Ekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806131,"Ekonomiservice, FHPT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806134,"Ekonomiservice, FNT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806132,"Ekonomiservice, FPV",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806133,"Ekonomiservice, FSE",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806130,"Ekonomiservice, central",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804121,Energi- och miljöteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804720,Energiteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,121,Energiteknik Vasa,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,3,Engelska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,10,Etnologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802130,Experience Lab,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280100,FHPT Stödtjänster,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801010,FHPT Uniservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801000,FHPT gemensamma kostnader,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801150,FHPT utbildning,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802000,FPV gemensamma kostnader,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280400,FNT Stödtjänster,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804000,FNT gemensamma kostnader,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804010,FNT universitetsservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802000,FPV Gemensamma kostnader,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280200,FPV Stödtjänster,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280220,FPV Vasa Övningsskola (VÖ),, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802010,FPV universitetsservice,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802100,Pedagogik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802110,Hälsovetenskaper,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802120,Socialvetenskaper,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802130,MediaCity,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802140,"Gamla forskarskolan, FPV",, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802150,FPV utbildning,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802200,"Vasa övningsskola, gemensamma kostnader",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802210,"Vasa övningsskola, grundutbildning",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802220,"Vasa övningsskola, gymnasiet",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803000,FSE gemensamma kostnader,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803000,FSE Gemensamma kostnader,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280300,FSE Stödtjänster,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803010,FSE universitetsservice,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803100,Företagsekonomi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803110,Nationalekonomi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803111,Informationsvetenskap,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803112,Informationssystem,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803113,IAMSR,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803120,Samhällsvetenskaper,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803121,Rättsvetenskap,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803122,Institutet för samhällsforskning,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803123,Institutet för mänskliga rättigheter,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803140,Doktorandnätverk Realizing Human Rights,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803141,Doktorandnätverk School of Business and Economics,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803142,"Gamla forskarskolan, FSE",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803143,Doktorandnätverk Citizens and Demogracy,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803150,FSE utbildning,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804000,FNT gemensamma kostnader,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804010,FNT universitetsservice,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804050,Tekniska serviceenheten,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804100,Naturvetenskaper,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804110,Kemi (teknisktvetenskapliga),, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804111,Kemi (naturvetenskapliga),, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804120,Process- och systemteknik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804121,Energi- och miljöteknik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804130,Naturmaterialteknik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804140,Doktorandnätverk Material Research,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804141,Doktorandnätverk Chemical Engineering,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804142,"Gamla forskarskolan, NatVet & teknik",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804150,"Utbildning, naturvetenskaper och teknik",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804160,Naturvetenskap och teknik gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804200,Nationalla PET-centret,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804300,Biokemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,91,Farmaci,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804310,Farmaci,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804311,Cellbiologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804312,Husö biologiska station,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804313,Miljö- och marinbiologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804340,Doktorandnätverk Informational and Structural Biology,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804341,Doktorandnätverk Molecular Biosciences,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804342,"Gamla forskarskolan, BioVet",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804343,Doktorandnätverket Functional Marine Biodiversity,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804350,"Utbildning, biovetenskaper",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804360,Biovetenskap gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804400,Bioteknikcentrum,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804410,"Gamla forskarskolan, BTC",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804500,Datateknik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804510,Datavetenskap,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804540,Doktorandnätverk Information Technologies and Mathematics,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804541,"Gamla forskarskolan, IT",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804550,"Utbildning, informationsteknologi",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804560,IT gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804600,Turku Centre for Computer Science,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804610,"Gamla forskarskolan, TUCS",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2805000,Centret för livslångt lärande,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2805010,Centret för språk och kommunikation,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806000,Åbo Akademis bibliotek,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806010,Tritonia,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806020,Sibeliusmuseum,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806030,Sjöhistoriska institutet,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806100,"Allmän universitetsservice, central",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806101,"Allmän universitetsservice, FHPT",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806102,"Allmän universitetsservice, FPV",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806103,"Allmän universitetsservice, FSE",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806104,"Allmän universitetsservice, FNT",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806110,Ledning,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806111,Kommunikation,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806120,Forskningsservice,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806121,"Utbildningsservice, central",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806122,"Utbildningsservice, FHPT",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806123,"Utbildningsservice, FPV",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806124,"Utbildningsservice, FSE",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806125,"Utbildningsservice, FNT",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806130,"Ekonomiservice, central",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806131,"Ekonomiservice, FHPT",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806132,"Ekonomiservice, FPV",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806133,"Ekonomiservice, FSE",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806134,"Ekonomiservice, FNT",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806140,Personalservice,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806150,Fastighetsservice och upphandling,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806151,Planering,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,138,Fastigheter,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806152,Fastigheter och hyror,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806160,ICT-service,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,1,Humanistiska fakulteten,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2,Humanistiska fakulteten gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,3,Engelska,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,4,Finska,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,5,Franska,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,6,Ryska,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,7,Svenska,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,8,Tyska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806150,Fastighetsservice och upphandling,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,116,Fiber- och cellulosateknologi,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,9,Filosofi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,10,Etnologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,4,Finska,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,11,Folkloristik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,143,Forskning och utbildning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806120,Forskningsservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,5,Franska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,81,Fysik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804700,Fysik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,83,Fysikalisk kemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,54,Företagets organisation och ledning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803100,Företagsekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,51,Företagsekonomiska ämnena,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,141,Förvaltningen gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804410,"Gamla forskarskolan, BTC",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804342,"Gamla forskarskolan, BioVet",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801143,"Gamla forskarskolan, FHPT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802140,"Gamla forskarskolan, FPV",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803142,"Gamla forskarskolan, FSE",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804541,"Gamla forskarskolan, IT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804142,"Gamla forskarskolan, NatVet & teknik",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804610,"Gamla forskarskolan, TUCS",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,37,Gammaltestamentlig exegetik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,84,Geologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804702,Geologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,49,Handelshögskolan,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,50,Handelshögskolan gemensamma,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,12,Historia,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,13,Religionsvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280110,"Humaniora, psykologi och teologi",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,1,Humanistiska fakulteten,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2,Humanistiska fakulteten gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,93,Husö biologiska station,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804312,Husö biologiska station,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802110,Hälsovetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803113,IAMSR,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806160,ICT service,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804721,Industriell Ekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,122,Industriell ekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,55,Informationsförvaltining,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,104,Informationssystem,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803112,Informationssystem,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804560,Informationsteknologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280450,Informationsteknologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803111,Informationsvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803123,Insitutet för mänskliga rättigheter,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,110,Insitutionen för kemiteknik gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,101,Instit. för informationsteknologi gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,105,Institute for Advanced Management System Research (IAMSR),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,69,Institutet för mänskliga rättiheter,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803122,Institutet för samhällsforskning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,88,Institutionen för biovetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,89,Institutionen för biovetenskaper gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,100,Institutionen för informationsteknologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,109,Institutionen för kemiteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,76,Institutionen för naturvetenskap gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,44,Institutionen för psykologi och logopedi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,53,Internationell marknadsföring,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,140,Kansler,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804111,Kemi (naturvetenskaplig),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804110,Kemi (tekn.vetenskaplig),, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280471,Kemi och kemiteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804713,Kemier gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,144,Kommunikation,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806111,Kommunikation,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,14,Konstvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801100,"Kultur, historia och filosofi",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,61,Kvinnovetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,38,Kyrkohistoria,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804050,Laboratorieservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806110,Ledning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806101,"Lednings- och fakultetsstöd, FHPT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806104,"Lednings- och fakultetsstöd, FNT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806102,"Lednings- och fakultetsstöd, FPV",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806103,"Lednings- och fakultetsstöd, FSE",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806100,"Lednings- och fakultetsstöd, central",, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,15,Litteraturvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,47,Logopedi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801111,Logopedi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,78,Matematik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804701,Matematik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,126,Mediacity,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,127,Mediacity,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,94,Miljö- och marinbiologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804313,Miljö- och marinbiologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804710,Molekylärvetenskap och teknik,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,16,Musikvetenskap,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,17,Sibeliusmuseum,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,18,Sjöhistoriska institutet,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,19,Dnätverk Art History,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,20,Pedagogiska fakulteten,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,56,Nationalekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803110,Nationalekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,86,Nationella PET centret,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,87,Nationella PET-Centret,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280420,Nationella PET-centret,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804200,Nationella PET-centret,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804130,Naturmaterialteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804711,Naturmaterialteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804100,Naturvetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280470,Naturvetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804704,Naturvetenskaper gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280410,Naturvetenskaper och teknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,77,Naturvetenskapliga institutionen,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804160,Natvet&Tek gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,41,Nytestamentlig exegetik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,74,Offentlig förvaltning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,112,Oorganisk kemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,82,Organisk kemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,117,Pappersförädling,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802100,Pedagogik,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,21,Pedagogik gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,22,Allmän pedagogik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,23,Barnpedagogik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,24,Specialpedagogik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,25,Vuxenpedagogik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,26,De matematisk-naturvetenskapliga ämnenas och idrottens didaktik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280210,Pedagogik och välfärd,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,27,"Pedagogik, tillämpad pedagogik och yrkesundervisningens didaktik",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,28,Slöjdpedagogik och husliga ekonomins didaktik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,29,Språk- och kulturvetenskapernas didaktik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,30,Vasa övningsskola,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,31,VÖS gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,32,"VÖS, åk 1-6",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,33,"VÖS, åk 7-9",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,34,"VÖS, gymnasiet",, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,35,Teologiska fakulteten,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,36,Teologiska fakulteten gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,37,Gammaltestamentlig exegetik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,38,Kyrkohistoria,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,39,Teologisk etik och religionsfilosofi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,20,Pedagogiska fakulteten,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,145,Personal,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806140,Personalservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806151,Planering,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,40,Praktisk teologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,41,Nytestamentlig exegetik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,42,Dogmatik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,43,Dnätverk Old Testament studies,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,44,Institutionen för psykologi och logopedi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,45,Psykologi och logopedi gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804722,Process och Systemteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804725,Process och energiteknik gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280472,Process- och energiteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804120,Process- och systemteknik,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,46,Psykologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,47,Logopedi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,48,Dnätverk Psychology and Logopedics,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,49,Handelshögskolan,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,50,Handelshögskolan gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,51,Företagsekonomiska ämnena,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801130,Psykologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,45,Psykologi och logopedi gemensamma,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,52,Redovisning,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,53,Internationell marknadsföring,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,54,Företagets organisation och ledning,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,55,Informationsförvaltining,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,56,Nationalekonomi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,57,Dnätverk School of Business and Economics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,147,Reform 2015,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,119,Reglerteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,139,Rektor,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280620,Rektors strategiska och centrala medel,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806200,Rektors strategiska och centrala medel,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,13,Religionsvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,6,Ryska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803121,Rättsvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,66,Rättsvetenskapliga institutionen,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,68,Rättvetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,67,Rättvetenskaper gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,75,Samforsk,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2803120,Samhällsvetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280310,Samhällsvetenskaper och ekonomi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,17,Sibeliusmuseum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806020,Sibeliusmuseum,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806030,Sjöhistoriska insitutet,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,18,Sjöhistoriska institutet,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,28,Slöjdpedagogik och husliga ekonomins didaktik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,63,Socialpolitik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802120,Socialvetenskaper,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,58,Socialvetenskapliga institutionen,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,59,Socialvetenskapliga institutionen gemensamma,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,60,Sociologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,61,Kvinnovetenskap,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,62,Vårdvetenskap,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,63,Socialpolitik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,64,Utvecklingspsykologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,65,Demografi och landsbygdsforskning,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,66,Rättsvetenskapliga institutionen,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,67,Rättvetenskaper gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,68,Rättvetenskaper,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,69,Institutet för mänskliga rättiheter,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,70,Dnätverk Realizing Human Rights,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,24,Specialpedagogik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801110,Språk,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,29,Språk- och kulturvetenskapernas didaktik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,80,Statistik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,73,Statskunskap,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,71,Statsvetenskapliga institutionen,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,72,Statsvetenskapliga institutionen gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,73,Statskunskap,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,74,Offentlig förvaltning,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,75,Samforsk,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,76,Institutionen för naturvetenskap gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,77,Naturvetenskapliga institutionen,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,78,Matematik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,79,Verkstaden,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,80,Statistik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,81,Fysik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,82,Organisk kemi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,83,Fysikalisk kemi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,84,Geologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,85,Dnätverk Material Research,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,86,Nationella PET centret,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,87,Nationella PET-Centret,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,88,Institutionen för biovetenskaper,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,89,Institutionen för biovetenskaper gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,90,Biokemi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,91,Farmaci,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,92,Cellbiologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,93,Husö biologiska station,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,94,Miljö- och marinbiologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,95,Biovetenskap,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,96,Dnätverk Informational and Structural Biology,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,97,Dnätverk Molecular Biosciences,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,98,Bioteknikcentrum,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,99,Bioteknikcentrum,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,100,Institutionen för informationsteknologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,101,Instit. för informationsteknologi gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,102,Datateknik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,103,Datavetenskap,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,104,Informationssystem,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,105,Institute for Advanced Management System Research (IAMSR),, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,106,Dnätverk IT and Mathematics,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,7,Svenska,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,107,TUCS,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,108,Åbo datatekniska forsknings- och utbildningscentrum (TUCS),, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,109,Institutionen för kemiteknik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,110,Insitutionen för kemiteknik gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,111,Analytisk kemi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,112,Oorganisk kemi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,113,Teknisk polymerkemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280460,TUCS,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804723,Teknisk Kemi och Reaktionsteknik,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,114,Teknisk kemi och reaktionsteknik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,115,Trä- och papperskemi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,116,Fiber- och cellulosateknologi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,117,Pappersförädling,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,118,Anläggningsteknik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,119,Reglerteknik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,120,Värmeteknik,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,121,Energiteknik Vasa,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,122,Industriell ekonomi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,123,Dnätverk Chemical Engineering,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,124,Centret för livslångt lärande,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,125,Centret för livslångt lärande (CLL),, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,126,Mediacity,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,127,Mediacity,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,128,Centret för språk och kommunikation,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,129,Centret för språk och kommunikation (CSK),, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,130,Datacentralen,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,131,Datacentralen,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,132,Åbo Akademis bibliotek,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,133,ÅAB,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,113,Teknisk polymerkemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,146,Teknisk service,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2801120,Teologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,39,Teologisk etik och religionsfilosofi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,35,Teologiska fakulteten,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,36,Teologiska fakulteten gemensamma,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,134,Tritonia,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806010,Tritonia,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,135,Tritonia 2011,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,115,Trä- och papperskemi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804600,Turku Centre for Computer Science,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,8,Tyska,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280610,Universitetsservice,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804712,Utbildning - Kemier,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804703,Utbildning - Naturvetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804724,Utbildning - Process och energiteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804350,Utbildning-Biovetenskaper,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804550,Utbildning-Informationsteknologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2804150,Utbildning-Naturvetenskaper och teknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806122,"Utbildningsservice, FHPT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806125,"Utbildningsservice, FNT",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806123,"Utbildningsservice, FPV",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806124,"Utbildningsservice, FSE",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806121,"Utbildningsservice, central",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,64,Utvecklingspsykologi,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,30,Vasa övningsskola,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,79,Verkstaden,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,25,Vuxenpedagogik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802200,VÖ Gemensamma kostnader,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802210,VÖ Grundutbildning,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2802220,VÖ Gymnasiet,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,31,VÖS gemensamma,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,34,"VÖS, gymnasiet",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,32,"VÖS, åk 1-6",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,33,"VÖS, åk 7-9",, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,120,Värmeteknik,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,62,Vårdvetenskap,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,2806000,Åbo Akademis Bibliotek,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,280600,Åbo Akademis Bibliotek,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,136,ÅA gemensamma,, Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,137,ÅA gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,138,Fastigheter,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,139,Rektor,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,140,Kansler,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,141,Förvaltningen gemensamma,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,142,Ekonomi,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,143,Forskning och utbildning,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,144,Kommunikation,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,145,Personal,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,146,Teknisk service,, -Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,147,Reform 2015,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,,,http://isni.org/isni/0000000123753425,7 -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,5040300,Tulosyksiköt,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030001,Geoenergia GNR,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030002,Yhdyskunnat ja rakentaminen YRA,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030003,Kalliorakentaminen ja sijoituspaikat KAS,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030004,Ympäristögeologia YMP,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030005,Merigeologia MRG,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030006,Mineraalitalous ja malmigeologia MIM,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030007,Mineraalivarannot MIV,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030008,Teollisuusmineraalit TMI,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030009,Mineraalitekniikka ja materiaalit MMA,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030010,Turvevarannot TUR,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030011,Pohjavesi PVI,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030012,Geofysiikan sovellukset GSO,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030013,Tuotantoympäristöt ja kierrätys TUK,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030014,Alueellinen geotieto ALG,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030015,Geotietovarannon hallinta GEH,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040300,504030016,Digitaaliset tuotteet ja palvelut DIP,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040200,5040200,Toiminnan johtaminen,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,5040200,50402003,Strateginen johto,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200310,"Henkilöstö, osaaminen ja työympäristöt",, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200320,Viestintä ja markkinointi,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200330,Strategia ja suunnittelu,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200340,Tieteellinen tutkimus,, -Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,50402003,5040200350,Digitaalisuus ja tietovarannot,, -Maanmittauslaitos,National Land Survey of Finland,Lantmäteriverket,4020217,,,,http://isni.org/isni/000000040494894X,1229 -Metsäntutkimuslaitos,Finnish Forest Research Institute,Skogsforskningsinstitutet,404001,,,,http://isni.org/isni/0000000122651136,TWV0c8OkbnR1dGtpbXVzbGFpdG9z -Suomen Pankki,Suomen Pankki,Finlands bank,02022481,,,,http://isni.org/isni/0000000404104982,1888 -Suomen ympäristökeskus,Finnish Environment Institute,Finlands miljöcentral,7020017,,,,http://isni.org/isni/0000000110191419,1903 -Säteilyturvakeskus,Radiation and Nuclear Safety Authority,Strålsäkerhetscentralen,5550012,,,,http://isni.org/isni/000000011534674X,U8OkdGVpbHl0dXJ2YWtlc2t1cw== -Teknologiantutkimuskeskus VTT Oy,VTT Technical Research Centre of Finland Ltd,Teknologiska forskningscentralen VTT AB,26473754,,,,http://isni.org/isni/0000000121061548,2045 -Terveyden ja hyvinvoinninlaitos,National Institute for Health and Welfare,Institutet för hälsa och välfärd,5610017,,,,http://isni.org/isni/0000000110130499,2060 -Työterveyslaitos,Finnish Institute of Occupational Health,Arbetshälsoinstitutet,02202669,,,,http://isni.org/isni/0000000404105926,VhnDtnRlcnZleXNsYWl0b3M= -Ulkopoliittinen instituutti,Finnish Institute of International Affairs,Utrikespolitiska institutet,1120017,,,,http://isni.org/isni/0000000406205576,2215 -Valtion taloudellinen tutkimuskeskus,VATT Institute for Economic Research,Statens ekonomiska forskningscentral,3060016,,,,http://isni.org/isni/0000000121621381,2396 -Tilastokeskus,Statistics Finland,Statistikcentralen,02454911,,,,http://isni.org/isni/0000000404105635, -Kotimaisten kielten keskus,Institute for the Languages of Finland,Institutet för de inhemska språken,02458728,,,,http://isni.org/isni/0000000109457291, -Maaseutuvirasto,Agency for Rural Affairs in Finland,Landsbygdsverket,24053596,,,,, -Luonnonvarakeskus,Natural Resources Institute Finland,Naturresursinstitutet,02446292,,,,http://isni.org/isni/0000000446686757, -Ilmatieteen laitos,Finnish Meteorological Insitute,Meterologiska institutet,02446647,,,,http://isni.org/isni/0000000122538678, -Väestörekisterikeskus,Population Register Center,Befolkningsregistercentralen,02454372,,,,, -CSC – Tieteen tietotekniikan keskus Oy,CSC - IT Center for Science Ltd,,09206320,,,,, -Väylävirasto,Finnish Transport Infrastructure Agency,Trafikledsverket,10105471,,,,http://isni.org/isni/0000000405091319, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,133,ÅAB,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,132,Åbo Akademis bibliotek,, +Åbo Akademi,Åbo Akademi University,Åbo Akademi,01903,,108,Åbo datatekniska forsknings- och utbildningscentrum (TUCS),, diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/elasticsearch_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/elasticsearch_service.py index d769e3e0..d3c03bef 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/service/elasticsearch_service.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/elasticsearch_service.py @@ -33,9 +33,7 @@ def index_exists(self, index): def create_index(self, index, filename): _logger.info("Trying to create index " + index) return self._operation_ok( - self.es.indices.create( - index=index, body=self._get_json_file_as_str(filename) - ) + self.es.indices.create(index=index, body=self._get_json_file_as_str(filename)) ) def delete_index(self, index): @@ -54,24 +52,16 @@ def delete_and_update_indexable_data(self, index, doc_type, indexable_data_list) ) self._delete_all_documents_from_index_with_type(index, doc_type) _logger.info( - "Trying to bulk update reference data with type " - + doc_type - + " to index " - + index + "Trying to bulk update reference data with type " + doc_type + " to index " + index ) - return self._operation_ok( - self.es.bulk(body=bulk_update_str, request_timeout=30) - ) + return self._operation_ok(self.es.bulk(body=bulk_update_str, request_timeout=30)) return None def _delete_all_documents_from_index_with_type(self, index, doc_type): _logger.info( - "Trying to delete all documents from index " - + index - + " having type " - + doc_type + "Trying to delete all documents from index " + index + " having type " + doc_type ) return self._operation_ok( self.es.delete_by_query( diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/finto_data_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/finto_data_service.py index 086e9c15..67f31e11 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/service/finto_data_service.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/finto_data_service.py @@ -75,13 +75,11 @@ def _parse_finto_data(self, graph, data_type): ) # parents (broader) parent_ids = [ - self._get_uri_end_part(parent) - for parent in graph.objects(concept, SKOS.broader) + self._get_uri_end_part(parent) for parent in graph.objects(concept, SKOS.broader) ] # children (narrower) child_ids = [ - self._get_uri_end_part(child) - for child in graph.objects(concept, SKOS.narrower) + self._get_uri_end_part(child) for child in graph.objects(concept, SKOS.narrower) ] same_as = [] wkt = "" @@ -145,13 +143,11 @@ def _fetch_finto_data(self, data_type): if not str_error: return g else: - _logger.error( - "Failed to read Finto data of type %s, skipping.." % data_type - ) + _logger.error("Failed to read Finto data of type %s, skipping.." % data_type) return None def _get_uri_end_part(self, uri): - return uri[uri.rindex("/") + 1:].strip() + return uri[uri.rindex("/") + 1 :].strip() def _get_coordinates_for_location_from_url(self, url): sleep_time = 2 diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/infra_data_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/infra_data_service.py index db118904..0a958fd9 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/service/infra_data_service.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/infra_data_service.py @@ -20,7 +20,9 @@ class InfraDataService: so it is first fetched and parsed. """ - INFRA_REF_DATA_SOURCE_URL = "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures" + INFRA_REF_DATA_SOURCE_URL = ( + "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures" + ) TEMP_FILENAME = "/tmp/data.json" diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/local_data_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/local_data_service.py index 24d99597..040ec24d 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/service/local_data_service.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/local_data_service.py @@ -19,7 +19,9 @@ class LocalDataService: # move infra here beacuse the fetch API for it is broken and there is no estimate when it could be fixed. # to keep the reference data unchanged, use the old scheme until some fix for it has been invented and # validated. - INFRA_SCHEME = "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures" + INFRA_SCHEME = ( + "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures" + ) def get_data(self, data_type): return self._parse_local_reference_data(data_type) diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/mime_data_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/mime_data_service.py index 9484292e..09eb70e8 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/service/mime_data_service.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/mime_data_service.py @@ -21,9 +21,7 @@ class MimeDataService: """ IANA_NS = "{http://www.iana.org/assignments}" - MIME_TYPE_REF_DATA_SOURCE_URL = ( - "https://www.iana.org/assignments/media-types/media-types.xml" - ) + MIME_TYPE_REF_DATA_SOURCE_URL = "https://www.iana.org/assignments/media-types/media-types.xml" MIME_TYPE_REGISTRY_IDS = [ "application", "audio", @@ -57,9 +55,7 @@ def _parse_mime_data(self): is_parsing_model_elem = False found_valid_file_elem = False found_valid_name_elem = False - for event, elem in ET.iterparse( - self.TEMP_XML_FILENAME, events=("start", "end") - ): + for event, elem in ET.iterparse(self.TEMP_XML_FILENAME, events=("start", "end")): if event == "start": if ( elem.tag == (self.IANA_NS + "registry") @@ -84,9 +80,7 @@ def _parse_mime_data(self): ): if elem.text: found_valid_file_elem = True - uri = ( - "https://www.iana.org/assignments/media-types/" + elem.text - ) + uri = "https://www.iana.org/assignments/media-types/" + elem.text data_id = elem.text elif event == "end": if elem.tag == self.IANA_NS + "registry": diff --git a/src/metax_api/tasks/refdata/refdata_indexer/service/organization_service.py b/src/metax_api/tasks/refdata/refdata_indexer/service/organization_service.py index 499d3e3f..fb75d0be 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/service/organization_service.py +++ b/src/metax_api/tasks/refdata/refdata_indexer/service/organization_service.py @@ -32,9 +32,7 @@ def get_data(self): same_as = org.get("same_as", []) org_csc = org.get("org_csc", "") index_data_models.append( - OrganizationData( - org["org_id"], org["label"], parent_id, same_as, org_csc - ) + OrganizationData(org["org_id"], org["label"], parent_id, same_as, org_csc) ) os.remove(self.INPUT_FILE) diff --git a/src/metax_api/templates/secure/auth_success.html b/src/metax_api/templates/secure/auth_success.html index 4c309deb..6e72f38a 100755 --- a/src/metax_api/templates/secure/auth_success.html +++ b/src/metax_api/templates/secure/auth_success.html @@ -2,6 +2,8 @@ + + Metax End User Authentication diff --git a/src/metax_api/templates/test_view.html b/src/metax_api/templates/test_view.html new file mode 100644 index 00000000..489966ac --- /dev/null +++ b/src/metax_api/templates/test_view.html @@ -0,0 +1,10 @@ + + + +

Test view page

+ +{{ cr.state }} + + + + diff --git a/src/metax_api/tests/api/oaipmh/minimal_api.py b/src/metax_api/tests/api/oaipmh/minimal_api.py index 15fdae60..e64b0776 100755 --- a/src/metax_api/tests/api/oaipmh/minimal_api.py +++ b/src/metax_api/tests/api/oaipmh/minimal_api.py @@ -23,27 +23,29 @@ class OAIPMHReadTests(APITestCase, TestClassUtils): - _namespaces = {'o': 'http://www.openarchives.org/OAI/2.0/', - 'oai_dc': "http://www.openarchives.org/OAI/2.0/oai_dc/", - 'dc': "http://purl.org/dc/elements/1.1/", - 'dct': "http://purl.org/dc/terms/", - 'datacite': 'http://schema.datacite.org/oai/oai-1.0/'} + _namespaces = { + "o": "http://www.openarchives.org/OAI/2.0/", + "oai_dc": "http://www.openarchives.org/OAI/2.0/oai_dc/", + "dc": "http://purl.org/dc/elements/1.1/", + "dct": "http://purl.org/dc/terms/", + "datacite": "http://schema.datacite.org/oai/oai-1.0/", + } @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(OAIPMHReadTests, cls).setUpClass() def setUp(self): cr = CatalogRecord.objects.get(pk=1) - cr.data_catalog.catalog_json['identifier'] = ATT_CATALOG + cr.data_catalog.catalog_json["identifier"] = ATT_CATALOG cr.data_catalog.force_save() cr = CatalogRecord.objects.get(pk=14) - cr.data_catalog.catalog_json['identifier'] = IDA_CATALOG + cr.data_catalog.catalog_json["identifier"] = IDA_CATALOG cr.data_catalog.force_save() # some cr that has publisher set... @@ -65,21 +67,21 @@ def _get_single_result(self, data, xpath): def _set_dataset_as_draft(self, cr_id): cr = CatalogRecord.objects.get(pk=cr_id) - cr.state = 'draft' + cr.state = "draft" cr.force_save() -# VERB: Identify + # VERB: Identify def test_identify(self): - response = self.client.get('/oai/?verb=Identify') + response = self.client.get("/oai/?verb=Identify") self.assertEqual(response.status_code, status.HTTP_200_OK) -# VERB: ListMetadataFormats + # VERB: ListMetadataFormats def test_list_metadata_formats(self): - response = self.client.get('/oai/?verb=ListMetadataFormats') + response = self.client.get("/oai/?verb=ListMetadataFormats") self.assertEqual(response.status_code, status.HTTP_200_OK) - formats = self._get_single_result(response.content, '//o:ListMetadataFormats') + formats = self._get_single_result(response.content, "//o:ListMetadataFormats") self.assertEqual(len(formats), 4) metadataPrefix = self._get_results(formats, '//o:metadataPrefix[text() = "oai_dc"]') @@ -88,322 +90,392 @@ def test_list_metadata_formats(self): metadataPrefix = self._get_results(formats, '//o:metadataPrefix[text() = "oai_datacite"]') self.assertEqual(len(metadataPrefix), 1) - metadataPrefix = self._get_results(formats, '//o:metadataPrefix[text() = "oai_dc_urnresolver"]') + metadataPrefix = self._get_results( + formats, '//o:metadataPrefix[text() = "oai_dc_urnresolver"]' + ) self.assertEqual(len(metadataPrefix), 1) -# VERB: ListSets + # VERB: ListSets def test_list_sets(self): - response = self.client.get('/oai/?verb=ListSets') + response = self.client.get("/oai/?verb=ListSets") self.assertEqual(response.status_code, status.HTTP_200_OK) - sets = self._get_results(response.content, '//o:setSpec') + sets = self._get_results(response.content, "//o:setSpec") # urnresolver set should be hidden self.assertEquals(len(sets), 4) -# VERB: ListIdentifiers + # VERB: ListIdentifiers def test_list_identifiers(self): - ms = settings.OAI['BATCH_SIZE'] + ms = settings.OAI["BATCH_SIZE"] allRecords = CatalogRecord.objects.filter( - data_catalog__catalog_json__identifier__in=MetaxOAIServer._get_default_set_filter())[:ms] - response = self.client.get('/oai/?verb=ListIdentifiers&metadataPrefix=oai_dc') + data_catalog__catalog_json__identifier__in=MetaxOAIServer._get_default_set_filter() + ) + response = self.client.get("/oai/?verb=ListIdentifiers&metadataPrefix=oai_dc") self.assertEqual(response.status_code, status.HTTP_200_OK) - headers = self._get_results(response.content, '//o:header') - self.assertTrue(len(headers) == len(allRecords), len(headers)) + headers = self._get_results(response.content, "//o:header") + self.assertTrue(len(headers) == ms, len(headers)) - response = self.client.get('/oai/?verb=ListIdentifiers&metadataPrefix=oai_datacite') + token = self._get_single_result(response.content, '//o:resumptionToken') + response = self.client.get(f"/oai/?verb=ListIdentifiers&resumptionToken={token.text}") self.assertEqual(response.status_code, status.HTTP_200_OK) - headers = self._get_results(response.content, '//o:header') - self.assertTrue(len(headers) == len(allRecords), len(headers)) - response = self.client.get('/oai/?verb=ListIdentifiers&metadataPrefix=oai_dc_urnresolver') + headers = self._get_results(response.content, "//o:header") + self.assertEqual(len(allRecords) - ms, len(headers)) + + response = self.client.get("/oai/?verb=ListIdentifiers&metadataPrefix=oai_datacite") + self.assertEqual(response.status_code, status.HTTP_200_OK) + headers = self._get_results(response.content, "//o:header") + self.assertTrue(len(headers) == ms, len(headers)) + + response = self.client.get("/oai/?verb=ListIdentifiers&metadataPrefix=oai_dc_urnresolver") self.assertEqual(response.status_code, status.HTTP_200_OK) errors = self._get_results(response.content, '//o:error[@code="badArgument"]') self.assertTrue(len(errors) == 1, response.content) def test_list_identifiers_for_drafts(self): - ''' Tests that drafts are not returned from ListIdentifiers ''' - ms = settings.OAI['BATCH_SIZE'] + """ Tests that drafts are not returned from ListIdentifiers """ + ms = settings.OAI["BATCH_SIZE"] allRecords = CatalogRecord.objects.filter( - data_catalog__catalog_json__identifier__in=MetaxOAIServer._get_default_set_filter())[:ms] + data_catalog__catalog_json__identifier__in=MetaxOAIServer._get_default_set_filter() + )[:ms] self._set_dataset_as_draft(25) self._set_dataset_as_draft(26) # headers should be reduced when some datasets are set as drafts - response = self.client.get('/oai/?verb=ListIdentifiers&metadataPrefix=oai_dc') + response = self.client.get("/oai/?verb=ListIdentifiers&metadataPrefix=oai_dc") self.assertEqual(response.status_code, status.HTTP_200_OK) - headers = self._get_results(response.content, '//o:header') + headers = self._get_results(response.content, "//o:header") self.assertFalse(len(headers) == len(allRecords), len(headers)) def test_list_identifiers_from_datacatalogs_set(self): - allRecords = DataCatalog.objects.all()[:settings.OAI['BATCH_SIZE']] - response = self.client.get('/oai/?verb=ListIdentifiers&metadataPrefix=oai_dc&set=datacatalogs') + allRecords = DataCatalog.objects.all()[: settings.OAI["BATCH_SIZE"]] + response = self.client.get( + "/oai/?verb=ListIdentifiers&metadataPrefix=oai_dc&set=datacatalogs" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - records = self._get_results(response.content, '//o:header') + records = self._get_results(response.content, "//o:header") self.assertTrue(len(records) == len(allRecords), len(records)) -# VERB: ListRecords + # VERB: ListRecords def test_list_records(self): - ms = settings.OAI['BATCH_SIZE'] + ms = settings.OAI["BATCH_SIZE"] allRecords = CatalogRecord.objects.filter( - data_catalog__catalog_json__identifier__in=MetaxOAIServer._get_default_set_filter())[:ms] + data_catalog__catalog_json__identifier__in=MetaxOAIServer._get_default_set_filter() + ) - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc') + response = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_dc") self.assertEqual(response.status_code, status.HTTP_200_OK) - records = self._get_results(response.content, '//o:record') - self.assertTrue(len(records) == len(allRecords)) + records = self._get_results(response.content, "//o:record") + self.assertTrue(len(records) == ms) - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_fairdata_datacite') + token = self._get_single_result(response.content, '//o:resumptionToken') + response = self.client.get(f"/oai/?verb=ListRecords&resumptionToken={token.text}") self.assertEqual(response.status_code, status.HTTP_200_OK) - records = self._get_results(response.content, '//o:record') - self.assertTrue(len(records) == len(allRecords)) - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc_urnresolver') + records = self._get_results(response.content, "//o:record") + self.assertTrue(len(allRecords) - ms == len(records)) + + response = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_fairdata_datacite") self.assertEqual(response.status_code, status.HTTP_200_OK) - records = self._get_results(response.content, '//o:record') - self.assertTrue(len(records) == len(allRecords)) + records = self._get_results(response.content, "//o:record") + self.assertTrue(len(records) == ms) + + response = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_dc_urnresolver") + self.assertEqual(response.status_code, status.HTTP_200_OK) + records = self._get_results(response.content, "//o:record") + self.assertTrue(len(records) == ms) def test_list_records_for_drafts(self): - ''' Tests that drafts are not returned from ListRecords ''' - ms = settings.OAI['BATCH_SIZE'] + """ Tests that drafts are not returned from ListRecords """ + ms = settings.OAI["BATCH_SIZE"] allRecords = CatalogRecord.objects.filter( - data_catalog__catalog_json__identifier__in=MetaxOAIServer._get_default_set_filter())[:ms] + data_catalog__catalog_json__identifier__in=MetaxOAIServer._get_default_set_filter() + )[:ms] self._set_dataset_as_draft(25) self._set_dataset_as_draft(26) - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_fairdata_datacite') + response = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_fairdata_datacite") self.assertEqual(response.status_code, status.HTTP_200_OK) - records = self._get_results(response.content, '//o:record') + records = self._get_results(response.content, "//o:record") self.assertFalse(len(records) == len(allRecords)) def test_list_records_urnresolver_from_datacatalogs_set(self): - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc_urnresolver&set=datacatalogs') + response = self.client.get( + "/oai/?verb=ListRecords&metadataPrefix=oai_dc_urnresolver&set=datacatalogs" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) errors = self._get_results(response.content, '//o:error[@code="badArgument"]') self.assertTrue(len(errors) == 1, response.content) - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc_urnresolver&set=ida_datasets') + response = self.client.get( + "/oai/?verb=ListRecords&metadataPrefix=oai_dc_urnresolver&set=ida_datasets" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) errors = self._get_results(response.content, '//o:error[@code="badArgument"]') self.assertTrue(len(errors) == 1, response.content) - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=invalid') + response = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=invalid") self.assertEqual(response.status_code, status.HTTP_200_OK) errors = self._get_results(response.content, '//o:error[@code="badArgument"]') self.assertTrue(len(errors) == 1, response.content) def test_list_records_urnresolver_for_datasets_set(self): - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc_urnresolver&set=datasets') + response = self.client.get( + "/oai/?verb=ListRecords&metadataPrefix=oai_dc_urnresolver&set=datasets" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - headers = self._get_results(response.content, '//o:header') + headers = self._get_results(response.content, "//o:header") self.assertTrue(len(headers) > 0) - records = self._get_results(response.content, '//oai_dc:dc') + records = self._get_results(response.content, "//oai_dc:dc") for record_metadata in records: - urn_elements = self._get_results(record_metadata, 'dc:identifier[starts-with(text(), "urn")]') + urn_elements = self._get_results( + record_metadata, 'dc:identifier[starts-with(text(), "urn")]' + ) self.assertTrue(urn_elements is not None or len(urn_elements) > 0) - url_element = self._get_single_result(record_metadata, 'dc:identifier[starts-with(text(), "http")]') + url_element = self._get_single_result( + record_metadata, 'dc:identifier[starts-with(text(), "http")]' + ) self.assertTrue(url_element is not None) def test_list_records_from_datasets_set(self): - ms = settings.OAI['BATCH_SIZE'] + ms = settings.OAI["BATCH_SIZE"] allRecords = CatalogRecord.objects.filter( - data_catalog__catalog_json__identifier__in=MetaxOAIServer._get_default_set_filter())[:ms] + data_catalog__catalog_json__identifier__in=MetaxOAIServer._get_default_set_filter() + )[:ms] - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=datasets') + response = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=datasets") self.assertEqual(response.status_code, status.HTTP_200_OK) - records = self._get_results(response.content, '//o:record') + records = self._get_results(response.content, "//o:record") self.assertTrue(len(records) == len(allRecords), len(records)) def test_list_records_from_datacatalogs_set(self): - allRecords = DataCatalog.objects.all()[:settings.OAI['BATCH_SIZE']] + allRecords = DataCatalog.objects.all()[: settings.OAI["BATCH_SIZE"]] - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=datacatalogs') + response = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=datacatalogs") self.assertEqual(response.status_code, status.HTTP_200_OK) - records = self._get_results(response.content, '//o:record') + records = self._get_results(response.content, "//o:record") self.assertTrue(len(records) == len(allRecords), len(records)) def test_list_records_from_att_datasets_set(self): allRecords = CatalogRecord.objects.filter( - data_catalog__catalog_json__identifier__in=[ATT_CATALOG])[:settings.OAI['BATCH_SIZE']] + data_catalog__catalog_json__identifier__in=[ATT_CATALOG] + )[: settings.OAI["BATCH_SIZE"]] - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=att_datasets') + response = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=att_datasets") self.assertEqual(response.status_code, status.HTTP_200_OK) - records = self._get_results(response.content, '//o:record') + records = self._get_results(response.content, "//o:record") self.assertTrue(len(records) == len(allRecords), len(records)) def test_list_records_from_ida_datasets_set(self): allRecords = CatalogRecord.objects.filter( - data_catalog__catalog_json__identifier__in=[IDA_CATALOG])[:settings.OAI['BATCH_SIZE']] + data_catalog__catalog_json__identifier__in=[IDA_CATALOG] + )[: settings.OAI["BATCH_SIZE"]] - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=ida_datasets') + response = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=ida_datasets") self.assertEqual(response.status_code, status.HTTP_200_OK) - records = self._get_results(response.content, '//o:record') + records = self._get_results(response.content, "//o:record") self.assertTrue(len(records) == len(allRecords)) def test_list_records_using_invalid_metadata_prefix(self): - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_notavailable') + response = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_notavailable") self.assertEqual(response.status_code, status.HTTP_200_OK) errors = self._get_results(response.content, '//o:error[@code="cannotDisseminateFormat"]') self.assertTrue(len(errors) == 1, response.content) response = self.client.get( - '/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_notavailable' % self.identifier) + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_notavailable" % self.identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) errors = self._get_results(response.content, '//o:error[@code="cannotDisseminateFormat"]') self.assertTrue(len(errors) == 1, response.content) def test_distinct_records_in_set(self): - att_resp = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=att_datasets') + att_resp = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=att_datasets") self.assertEqual(att_resp.status_code, status.HTTP_200_OK) - ida_resp = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=ida_datasets') + ida_resp = self.client.get("/oai/?verb=ListRecords&metadataPrefix=oai_dc&set=ida_datasets") self.assertEqual(ida_resp.status_code, status.HTTP_200_OK) - att_records = self._get_results(att_resp.content, '//o:record') + att_records = self._get_results(att_resp.content, "//o:record") att_identifier = att_records[0][0][0].text - ida_records = self._get_results(ida_resp.content, - '//o:record/o:header/o:identifier[text()="%s"]' % att_identifier) + ida_records = self._get_results( + ida_resp.content, + '//o:record/o:header/o:identifier[text()="%s"]' % att_identifier, + ) self.assertTrue(len(ida_records) == 0) -# VERB: GetRecord + # VERB: GetRecord def test_record_get_datacatalog(self): dc = DataCatalog.objects.get(pk=1) - dc_identifier = dc.catalog_json['identifier'] + dc_identifier = dc.catalog_json["identifier"] response = self.client.get( - '/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc' % dc_identifier) + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc" % dc_identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - identifiers = self._get_results(response.content, - '//o:record/o:metadata/oai_dc:dc/dc:identifier[text()="%s"]' % - dc_identifier) + identifiers = self._get_results( + response.content, + '//o:record/o:metadata/oai_dc:dc/dc:identifier[text()="%s"]' % dc_identifier, + ) self.assertTrue(len(identifiers) == 1, response.content) def test_get_record(self): response = self.client.get( - '/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc' % self.identifier) + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc" % self.identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - identifiers = self._get_results(response.content, - '//o:record/o:header/o:identifier[text()="%s"]' % self.identifier) + identifiers = self._get_results( + response.content, + '//o:record/o:header/o:identifier[text()="%s"]' % self.identifier, + ) self.assertTrue(len(identifiers) == 1, response.content) response = self.client.get( - '/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_fairdata_datacite' % self.identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) - identifiers = self._get_results(response.content, - '//o:record/o:metadata/datacite:oai_fairdata_datacite/' + - 'datacite:schemaVersion[text()="%s"]' % '4.1') + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_fairdata_datacite" + % self.identifier + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + identifiers = self._get_results( + response.content, + "//o:record/o:metadata/datacite:oai_fairdata_datacite/" + + 'datacite:schemaVersion[text()="%s"]' % "4.1", + ) self.assertTrue(len(identifiers) == 1, response.content) - identifiers = self._get_results(response.content, - '//o:record/o:header/o:identifier[text()="%s"]' % self.identifier) + identifiers = self._get_results( + response.content, + '//o:record/o:header/o:identifier[text()="%s"]' % self.identifier, + ) self.assertTrue(len(identifiers) == 1, response.content) def test_get_record_for_drafts(self): - ''' Tests that GetRecord doesn't return drafts ''' + """ Tests that GetRecord doesn't return drafts """ response = self.client.get( - '/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc' % self.identifier) + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc" % self.identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - identifiers = self._get_results(response.content, - '//o:record/o:header/o:identifier[text()="%s"]' % self.identifier) + identifiers = self._get_results( + response.content, + '//o:record/o:header/o:identifier[text()="%s"]' % self.identifier, + ) self.assertTrue(len(identifiers) == 1, response.content) # Set same dataset as draft self._set_dataset_as_draft(self.id) response = self.client.get( - '/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc' % self.identifier) + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc" % self.identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - identifiers = self._get_results(response.content, - '//o:record/o:header/o:identifier[text()="%s"]' % self.identifier) + identifiers = self._get_results( + response.content, + '//o:record/o:header/o:identifier[text()="%s"]' % self.identifier, + ) self.assertTrue(len(identifiers) == 0, response.content) def test_get_record_non_existing(self): - response = self.client.get('/oai/?verb=GetRecord&identifier=urn:non:existing&metadataPrefix=oai_dc') + response = self.client.get( + "/oai/?verb=GetRecord&identifier=urn:non:existing&metadataPrefix=oai_dc" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) errors = self._get_results(response.content, '//o:error[@code="idDoesNotExist"]') self.assertTrue(len(errors) == 1, response.content) def test_get_record_urnresolver(self): response = self.client.get( - '/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc_urnresolver' % self.identifier) + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc_urnresolver" % self.identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) errors = self._get_results(response.content, '//o:error[@code="badArgument"]') self.assertTrue(len(errors) == 1, response.content) def test_get_record_datacatalog_unsupported_in_urnresolver(self): dc = DataCatalog.objects.get(pk=1) - dc_identifier = dc.catalog_json['identifier'] + dc_identifier = dc.catalog_json["identifier"] response = self.client.get( - '/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc_urnresolver' % dc_identifier) + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc_urnresolver" % dc_identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) errors = self._get_results(response.content, '//o:error[@code="badArgument"]') self.assertTrue(len(errors) == 1, response.content) def test_get_record_datacatalog_unsupported_in_datacite(self): dc = DataCatalog.objects.get(pk=1) - dc_identifier = dc.catalog_json['identifier'] + dc_identifier = dc.catalog_json["identifier"] response = self.client.get( - '/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_datacite' % dc_identifier) + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_datacite" % dc_identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) errors = self._get_results(response.content, '//o:error[@code="badArgument"]') self.assertTrue(len(errors) == 1, response.content) def test_get_record_legacy_catalog_datasets_are_not_urnresolved(self): cr = CatalogRecord.objects.get(identifier=self.identifier) - cr.data_catalog.catalog_json['identifier'] = settings.LEGACY_CATALOGS[0] + cr.data_catalog.catalog_json["identifier"] = settings.LEGACY_CATALOGS[0] cr.data_catalog.force_save() response = self.client.get( - '/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc_urnresolver&set=datasets' % self.identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) - identifiers = self._get_results(response.content, - '//o:record/o:metadata/oai_dc:dc/dc:identifier[text()="%s"]' % - self.preferred_identifier) + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc_urnresolver&set=datasets" + % self.identifier + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + identifiers = self._get_results( + response.content, + '//o:record/o:metadata/oai_dc:dc/dc:identifier[text()="%s"]' + % self.preferred_identifier, + ) self.assertTrue(len(identifiers) == 0, response.content) -# OAI-PMH Utilities & functionalities + # OAI-PMH Utilities & functionalities def test_write_oai_dc_with_lang(self): from metax_api.api.oaipmh.base.view import oai_dc_writer_with_lang + e = Element("Test") md = { - 'title': [{'value': 'title1', 'lang': 'en'}, {'value': 'title2', 'lang': 'fi'}], - 'description': [{'value': 'value'}] + "title": [ + {"value": "title1", "lang": "en"}, + {"value": "title2", "lang": "fi"}, + ], + "description": [{"value": "value"}], } - metadata = common.Metadata('', md) + metadata = common.Metadata("", md) oai_dc_writer_with_lang(e, metadata) result = str(lxml.etree.tostring(e, pretty_print=True)) self.assertTrue('title1' in result) self.assertTrue('title2' in result) - self.assertTrue('value' in result) + self.assertTrue("value" in result) def test_get_oai_dc_metadata_dataset(self): cr = CatalogRecord.objects.get(pk=11) from metax_api.api.oaipmh.base.metax_oai_server import MetaxOAIServer + s = MetaxOAIServer() md = s._get_oai_dc_metadata(cr, cr.research_dataset) - self.assertTrue('identifier' in md) - self.assertTrue('title' in md) - self.assertTrue('lang' in md['title'][0]) + self.assertTrue("identifier" in md) + self.assertTrue("title" in md) + self.assertTrue("lang" in md["title"][0]) def test_get_oai_dc_metadata_datacatalog(self): dc = DataCatalog.objects.get(pk=1) from metax_api.api.oaipmh.base.metax_oai_server import MetaxOAIServer + s = MetaxOAIServer() md = s._get_oai_dc_metadata(dc, dc.catalog_json) - self.assertTrue('identifier' in md) - self.assertTrue('title' in md) - self.assertTrue('lang' in md['title'][0]) + self.assertTrue("identifier" in md) + self.assertTrue("title" in md) + self.assertTrue("lang" in md["title"][0]) def test_sensitive_fields_are_removed(self): """ Ensure some sensitive fields are never present in output of OAI-PMH apis """ - sensitive_field_values = ['email@mail.com', '999-123-123', '999-456-456'] + sensitive_field_values = ["email@mail.com", "999-123-123", "999-456-456"] def _check_fields(content): """ @@ -411,21 +483,30 @@ def _check_fields(content): of field name, since the field names might be different in Datacite etc other formats. """ for sensitive_field_value in sensitive_field_values: - self.assertEqual(sensitive_field_value not in str(content), True, - 'field %s should have been stripped' % sensitive_field_value) + self.assertEqual( + sensitive_field_value not in str(content), + True, + "field %s should have been stripped" % sensitive_field_value, + ) for cr in CatalogRecord.objects.filter(identifier=self.identifier): - cr.research_dataset['curator'][0].update({ - 'email': sensitive_field_values[0], - 'phone': sensitive_field_values[1], - 'telephone': sensitive_field_values[2], - }) + cr.research_dataset["curator"][0].update( + { + "email": sensitive_field_values[0], + "phone": sensitive_field_values[1], + "telephone": sensitive_field_values[2], + } + ) cr.force_save() - response = self.client.get('/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc' % self.identifier) + response = self.client.get( + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_dc" % self.identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) _check_fields(response.content) - response = self.client.get('/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_datacite' % self.identifier) + response = self.client.get( + "/oai/?verb=GetRecord&identifier=%s&metadataPrefix=oai_datacite" % self.identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) _check_fields(response.content) diff --git a/src/metax_api/tests/api/oaipmh/syke.py b/src/metax_api/tests/api/oaipmh/syke.py index 647469a4..2c0622a2 100755 --- a/src/metax_api/tests/api/oaipmh/syke.py +++ b/src/metax_api/tests/api/oaipmh/syke.py @@ -17,35 +17,35 @@ class SYKEOAIPMHReadTests(APITestCase, TestClassUtils): - _namespaces = {'o': 'http://www.openarchives.org/OAI/2.0/', - 'oai_dc': "http://www.openarchives.org/OAI/2.0/oai_dc/", - 'dc': "http://purl.org/dc/elements/1.1/", - 'dct': "http://purl.org/dc/terms/", - 'datacite': 'http://schema.datacite.org/oai/oai-1.0/'} + _namespaces = { + "o": "http://www.openarchives.org/OAI/2.0/", + "oai_dc": "http://www.openarchives.org/OAI/2.0/oai_dc/", + "dc": "http://purl.org/dc/elements/1.1/", + "dct": "http://purl.org/dc/terms/", + "datacite": "http://schema.datacite.org/oai/oai-1.0/", + } @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(SYKEOAIPMHReadTests, cls).setUpClass() def setUp(self): cr = CatalogRecord.objects.get(pk=1) cr.data_catalog.catalog_json["identifier"] = "urn:nbn:fi:att:data-catalog-harvest-syke" cr.data_catalog.force_save() - cr.research_dataset.update({ - "preferred_identifier": "urn:nbn:fi:csc-kata20170613100856741858", - "other_identifier": [ - { - "notation": "{55AB842F-9CED-4E80-A7E5-07A54F0AE4A4}" - } - ] - }) + cr.research_dataset.update( + { + "preferred_identifier": "urn:nbn:fi:csc-kata20170613100856741858", + "other_identifier": [{"notation": "{55AB842F-9CED-4E80-A7E5-07A54F0AE4A4}"}], + } + ) cr.force_save() self.identifier = cr.identifier - self.pref_identifier = cr.research_dataset['preferred_identifier'] + self.pref_identifier = cr.research_dataset["preferred_identifier"] self.dc = cr.data_catalog.catalog_json["identifier"] self._use_http_authorization() @@ -56,13 +56,19 @@ def _get_results(self, data, xpath): return root.xpath(xpath, namespaces=self._namespaces) def test_get_urn_resolver_record(self): - response = self.client.get('/oai/?verb=ListRecords&metadataPrefix=oai_dc_urnresolver&set=datasets') + response = self.client.get( + "/oai/?verb=ListRecords&metadataPrefix=oai_dc_urnresolver&set=datasets" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - identifiers = self._get_results(response.content, - '//o:record/o:metadata/oai_dc:dc/dc:identifier[text()="%s"]' % self.pref_identifier) + identifiers = self._get_results( + response.content, + '//o:record/o:metadata/oai_dc:dc/dc:identifier[text()="%s"]' % self.pref_identifier, + ) self.assertTrue(len(identifiers) == 1, response.content) - syke_url = SYKE_URL_PREFIX_TEMPLATE % '{55AB842F-9CED-4E80-A7E5-07A54F0AE4A4}' - identifiers = self._get_results(response.content, - '//o:record/o:metadata/oai_dc:dc/dc:identifier[text()="%s"]' % syke_url) + syke_url = SYKE_URL_PREFIX_TEMPLATE % "{55AB842F-9CED-4E80-A7E5-07A54F0AE4A4}" + identifiers = self._get_results( + response.content, + '//o:record/o:metadata/oai_dc:dc/dc:identifier[text()="%s"]' % syke_url, + ) self.assertTrue(len(identifiers) == 1, response.content) diff --git a/src/metax_api/tests/api/rest/base/serializers/file_serializer.py b/src/metax_api/tests/api/rest/base/serializers/file_serializer.py index 5db7e04d..ab40a016 100755 --- a/src/metax_api/tests/api/rest/base/serializers/file_serializer.py +++ b/src/metax_api/tests/api/rest/base/serializers/file_serializer.py @@ -16,26 +16,27 @@ class LightFileSerializerTests(APITestCase, TestClassUtils): - @classmethod def setUpClass(cls): - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super().setUpClass() def test_ls_field_list(self): - lfs_field_list = LightFileSerializer.ls_field_list(['identifier', 'parent_directory', 'file_storage']) - self.assertEqual('parent_directory__identifier' in lfs_field_list, True) - self.assertEqual('file_storage__file_storage_json' in lfs_field_list, True) + lfs_field_list = LightFileSerializer.ls_field_list( + ["identifier", "parent_directory", "file_storage"] + ) + self.assertEqual("parent_directory__identifier" in lfs_field_list, True) + self.assertEqual("file_storage__file_storage_json" in lfs_field_list, True) queryset = File.objects.filter(id__in=[1, 2]).values(*lfs_field_list) lfs_output = LightFileSerializer.serialize(queryset) self.assertEqual(len(lfs_output), 2) f = lfs_output[0] - self.assertEqual('parent_directory' in f, True) - self.assertEqual('identifier' in f['parent_directory'], True) - self.assertEqual('file_storage' in f, True) - self.assertEqual('identifier' in f['file_storage'], True) + self.assertEqual("parent_directory" in f, True) + self.assertEqual("identifier" in f["parent_directory"], True) + self.assertEqual("file_storage" in f, True) + self.assertEqual("identifier" in f["file_storage"], True) def test_serializer_outputs_are_the_same(self): fs_output = FileSerializer(File.objects.get(pk=1)).data @@ -43,7 +44,7 @@ def test_serializer_outputs_are_the_same(self): lfs_field_list = LightFileSerializer.ls_field_list() lfs_output = LightFileSerializer.serialize(File.objects.values(*lfs_field_list).get(pk=1)) - self.assertEqual(len(fs_output), len(lfs_output), 'number of keys should match') + self.assertEqual(len(fs_output), len(lfs_output), "number of keys should match") self._assert_keys_match_in_dict(fs_output, lfs_output) def test_serializer_error_reporting(self): @@ -57,11 +58,11 @@ def test_serializer_error_reporting(self): def _assert_keys_match_in_dict(self, ser_dict, ls_dict): for key, value in ser_dict.items(): - self.assertEqual(key in ls_dict, True, 'key should be present in boths outputs') + self.assertEqual(key in ls_dict, True, "key should be present in boths outputs") if isinstance(ls_dict[key], datetime): # in the api datetimes are converted to str by django later - probably the value is ok here pass elif isinstance(value, dict): self._assert_keys_match_in_dict(value, ls_dict[key]) else: - self.assertEqual(value, ls_dict[key], 'value should be same both outputs') + self.assertEqual(value, ls_dict[key], "value should be same both outputs") diff --git a/src/metax_api/tests/api/rest/base/serializers/serializer_utils.py b/src/metax_api/tests/api/rest/base/serializers/serializer_utils.py index 859708cf..4f45e303 100755 --- a/src/metax_api/tests/api/rest/base/serializers/serializer_utils.py +++ b/src/metax_api/tests/api/rest/base/serializers/serializer_utils.py @@ -13,7 +13,7 @@ from metax_api.models import CatalogRecord from metax_api.tests.utils import TestClassUtils, get_json_schema, test_data_file_path -schema = get_json_schema('ida_dataset') +schema = get_json_schema("ida_dataset") class ValidateJsonTests(APITestCase, TestClassUtils): @@ -24,7 +24,7 @@ class ValidateJsonTests(APITestCase, TestClassUtils): @classmethod def setUpClass(cls): - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super().setUpClass() def test_validate_date_field(self): @@ -33,27 +33,27 @@ def test_validate_date_field(self): have the optional "format": "date" applied, and use a format checker during schema validation. Ensure it works. """ - rd = CatalogRecord.objects.values('research_dataset').get(pk=1)['research_dataset'] + rd = CatalogRecord.objects.values("research_dataset").get(pk=1)["research_dataset"] - rd['issued'] = '2018-09-29' + rd["issued"] = "2018-09-29" try: validate_json(rd, schema) except ValidationError: - self.fail('Validation raised ValidationError - should have validated ok') + self.fail("Validation raised ValidationError - should have validated ok") - rd['issued'] = 'absolutely should fail' + rd["issued"] = "absolutely should fail" with self.assertRaises(ValidationError): validate_json(rd, schema) - rd['issued'] = '2018-09-29T20:20:20+03:00' + rd["issued"] = "2018-09-29T20:20:20+03:00" with self.assertRaises(ValidationError): validate_json(rd, schema) - rd['issued'] = '2018-09-29T20:20:20' + rd["issued"] = "2018-09-29T20:20:20" with self.assertRaises(ValidationError): validate_json(rd, schema) - rd['issued'] = '2018-09-29 20:20:20' + rd["issued"] = "2018-09-29 20:20:20" with self.assertRaises(ValidationError): validate_json(rd, schema) @@ -63,37 +63,37 @@ def test_validate_date_time_field(self): have the optional "format": "date-time" applied, and use a format checker during schema validation. Ensure it works. """ - rd = CatalogRecord.objects.values('research_dataset').get(pk=1)['research_dataset'] + rd = CatalogRecord.objects.values("research_dataset").get(pk=1)["research_dataset"] - rd['modified'] = '2018-09-29T20:20:20+03:00' + rd["modified"] = "2018-09-29T20:20:20+03:00" try: validate_json(rd, schema) except ValidationError: - self.fail('Validation raised ValidationError - should have validated ok') + self.fail("Validation raised ValidationError - should have validated ok") # note: jsonschema.FormatChecker accepts below also - rd['modified'] = '2018-09-29T20:20:20.123456789+03:00' + rd["modified"] = "2018-09-29T20:20:20.123456789+03:00" try: validate_json(rd, schema) except ValidationError: - self.fail('Validation raised ValidationError - should have validated ok') + self.fail("Validation raised ValidationError - should have validated ok") - rd['modified'] = 'absolutely should fail' + rd["modified"] = "absolutely should fail" with self.assertRaises(ValidationError): validate_json(rd, schema) - rd['modified'] = '2018-09-29' + rd["modified"] = "2018-09-29" with self.assertRaises(ValidationError): validate_json(rd, schema) - rd['modified'] = '2018-09-29 20:20:20' + rd["modified"] = "2018-09-29 20:20:20" with self.assertRaises(ValidationError): validate_json(rd, schema) - rd['modified'] = '2018-09-29T20:20:20' + rd["modified"] = "2018-09-29T20:20:20" with self.assertRaises(ValidationError): validate_json(rd, schema) - rd['modified'] = '2018-09-29 20:20:20+03:00' + rd["modified"] = "2018-09-29 20:20:20+03:00" with self.assertRaises(ValidationError): validate_json(rd, schema) diff --git a/src/metax_api/tests/api/rest/base/views/apierrors/__init__.py b/src/metax_api/tests/api/rest/base/views/apierrors/__init__.py deleted file mode 100755 index 0ffba528..00000000 --- a/src/metax_api/tests/api/rest/base/views/apierrors/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from .read import * diff --git a/src/metax_api/tests/api/rest/base/views/apierrors/read.py b/src/metax_api/tests/api/rest/base/views/apierrors/read.py deleted file mode 100755 index da61ec61..00000000 --- a/src/metax_api/tests/api/rest/base/views/apierrors/read.py +++ /dev/null @@ -1,183 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT -import logging -from os import makedirs -from shutil import rmtree - -from django.conf import settings -from django.core.management import call_command -from rest_framework import status -from rest_framework.test import APITestCase - -from metax_api.tests.utils import TestClassUtils, test_data_file_path, testcase_log_console - -_logger = logging.getLogger(__name__) - - -class ApiErrorReadBasicTests(APITestCase, TestClassUtils): - - """ - Basic read operations - """ - - @classmethod - def setUpClass(cls): - """ - Loaded only once for test cases inside this class. - """ - call_command("loaddata", test_data_file_path, verbosity=0) - super(ApiErrorReadBasicTests, cls).setUpClass() - - def setUp(self): - super(ApiErrorReadBasicTests, self).setUp() - rmtree(settings.ERROR_FILES_PATH, ignore_errors=True) - makedirs(settings.ERROR_FILES_PATH) - self._use_http_authorization( - username="metax" - ) - - def _assert_fields_presence(self, response): - """ - Check presence and absence of some key information. - """ - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual("data" in response.data, True, response.data) - self.assertEqual("response" in response.data, True, response.data) - self.assertEqual("traceback" in response.data, True, response.data) - self.assertEqual("url" in response.data, True, response.data) - self.assertEqual( - "HTTP_AUTHORIZATION" in response.data["headers"], - False, - response.data["headers"], - ) - - def test_list_errors(self): - """ - Each requesting resulting in an error should leave behind one API error entry. - """ - cr_1 = self.client.get("/rest/datasets/1").data - cr_1.pop("id") - cr_1.pop("identifier") - cr_1.pop("data_catalog") # causes an error - - response = self.client.post("/rest/datasets", cr_1, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.post("/rest/datasets", cr_1, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - response = self.client.get("/rest/apierrors") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_get_error_details(self): - cr_1 = self.client.get("/rest/datasets/1").data - cr_1.pop("id") - cr_1.pop("identifier") - cr_1.pop("data_catalog") # causes an error - cr_1["research_dataset"]["title"] = {"en": "Abc"} - - response = self.client.post("/rest/datasets", cr_1, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # list errors in order to get error identifier - response = self.client.get("/rest/apierrors") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual("identifier" in response.data[0], True, response.data) - - response = self.client.get( - "/rest/apierrors/%s" % response.data[0]["identifier"] - ) - self._assert_fields_presence(response) - self.assertEqual( - "data_catalog" in response.data["response"], True, response.data["response"] - ) - self.assertEqual( - response.data["data"]["research_dataset"]["title"]["en"], - "Abc", - response.data["data"]["research_dataset"]["title"], - ) - - @testcase_log_console(_logger) - def test_delete_error_details(self): - cr_1 = self.client.get("/rest/datasets/1").data - cr_1.pop("id") - cr_1.pop("identifier") - cr_1.pop("data_catalog") # causes an error - - response = self.client.post("/rest/datasets", cr_1, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - response = self.client.get("/rest/apierrors") - response = self.client.delete( - "/rest/apierrors/%s" % response.data[0]["identifier"] - ) - self.assertEqual( - response.status_code, status.HTTP_204_NO_CONTENT, response.data - ) - - response = self.client.get("/rest/apierrors") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - @testcase_log_console(_logger) - def test_delete_all_error_details(self): - cr_1 = self.client.get("/rest/datasets/1").data - cr_1.pop("id") - cr_1.pop("identifier") - cr_1.pop("data_catalog") # causes an error - - response = self.client.post("/rest/datasets", cr_1, format="json") - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - response = self.client.post("/rest/datasets", cr_1, format="json") - self.assertEqual( - response.status_code, status.HTTP_400_BAD_REQUEST, response.data - ) - - # ensure something was produced... - response = self.client.get("/rest/apierrors") - - response = self.client.post("/rest/apierrors/flush") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - response = self.client.get("/rest/apierrors") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_bulk_operation_produces_error_entry(self): - """ - Ensure also bulk operations produce error entries. - """ - cr_1 = self.client.get("/rest/datasets/1").data - cr_1.pop("id") - cr_1.pop("identifier") - cr_1.pop("data_catalog") # causes an error - response = self.client.post("/rest/datasets", [cr_1, cr_1], format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - response = self.client.get("/rest/apierrors") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - response = self.client.get( - "/rest/apierrors/%s" % response.data[0]["identifier"] - ) - self._assert_fields_presence(response) - self.assertEqual("other" in response.data, True, response.data) - self.assertEqual("bulk_request" in response.data["other"], True, response.data) - self.assertEqual( - "data_row_count" in response.data["other"], True, response.data - ) - - def test_api_permitted_only_to_metax_user(self): - # uses testuser by default - self._use_http_authorization() - response = self.client.get("/rest/apierrors") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.get("/rest/apierrors/123") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.delete("/rest/apierrors/123") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.post("/rest/apierrors/flush_errors") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/src/metax_api/tests/api/rest/base/views/common/auth.py b/src/metax_api/tests/api/rest/base/views/common/auth.py index 45bae99a..500e8d8a 100755 --- a/src/metax_api/tests/api/rest/base/views/common/auth.py +++ b/src/metax_api/tests/api/rest/base/views/common/auth.py @@ -75,6 +75,15 @@ def test_delete_access_error(self): response = self.client.delete("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_published(self): + """ + Unauthenticated users should only be able to see published datasets. + """ + self.client._credentials = {} + response = self.client.get("/rest/datasets") + for cr in response.data['results']: + self.assertEqual(cr['state'], 'published') + def test_read_for_datasets_world_ok(self): """ Reading datasets api should be permitted even without any authorization. @@ -179,9 +188,7 @@ class ApiEndUserAdditionalProjects(CatalogRecordApiWriteCommon): def setUp(self): super().setUp() - self._use_http_authorization( - method="bearer", token=get_test_oidc_token(new_proxy=True) - ) + self._use_http_authorization(method="bearer", token=get_test_oidc_token(new_proxy=True)) self._mock_token_validation_succeeds() def tearDown(self): @@ -189,9 +196,7 @@ def tearDown(self): try: os.remove(settings.ADDITIONAL_USER_PROJECTS_PATH) except: - _logger.info( - "error removing file from %s" % settings.ADDITIONAL_USER_PROJECTS_PATH - ) + _logger.info("error removing file from %s" % settings.ADDITIONAL_USER_PROJECTS_PATH) @responses.activate def test_successful_read(self): @@ -203,9 +208,7 @@ def test_successful_read(self): json.dump(testdata, testfile, indent=4) os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - response = self.client.get( - "/rest/files?project_identifier=project_x", format="json" - ) + response = self.client.get("/rest/files?project_identifier=project_x", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) @responses.activate @@ -213,9 +216,7 @@ def test_no_file(self): """ Projects are fetched from token when local file is not available. """ - response = self.client.get( - "/rest/files?project_identifier=2001036", format="json" - ) + response = self.client.get("/rest/files?project_identifier=2001036", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) @responses.activate @@ -228,9 +229,7 @@ def test_bad_file_keys(self): json.dump(testdata, testfile, indent=4) os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - response = self.client.get( - "/rest/files?project_identifier=project_x", format="json" - ) + response = self.client.get("/rest/files?project_identifier=project_x", format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) @responses.activate @@ -243,9 +242,7 @@ def test_bad_file_values(self): json.dump(testdata, testfile, indent=4) os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - response = self.client.get( - "/rest/files?project_identifier=project_x", format="json" - ) + response = self.client.get("/rest/files?project_identifier=project_x", format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) @responses.activate @@ -258,7 +255,5 @@ def test_bad_file_successful(self): json.dump(testdata, testfile, indent=4) os.chmod(settings.ADDITIONAL_USER_PROJECTS_PATH, 0o400) - response = self.client.get( - "/rest/files?project_identifier=2001036", format="json" - ) + response = self.client.get("/rest/files?project_identifier=2001036", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) diff --git a/src/metax_api/tests/api/rest/base/views/common/read.py b/src/metax_api/tests/api/rest/base/views/common/read.py index 9048aa5f..6d4887d7 100755 --- a/src/metax_api/tests/api/rest/base/views/common/read.py +++ b/src/metax_api/tests/api/rest/base/views/common/read.py @@ -28,11 +28,11 @@ def test_removed_query_param(self): obj2 = CatalogRecord.objects.get(pk=2) obj2.removed = True obj2.force_save() - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - response = self.client.get('/rest/datasets/1?removed=true') + response = self.client.get("/rest/datasets/1?removed=true") self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.get('/rest/datasets/metadata_version_identifiers') + response = self.client.get("/rest/datasets/metadata_version_identifiers") self.assertEqual(obj.metadata_version_identifier not in response.data, True) self.assertEqual(obj2.metadata_version_identifier not in response.data, True) @@ -42,31 +42,39 @@ def test_removed_query_param(self): obj2 = File.objects.get(pk=2) obj2.removed = True obj2.force_save() - response = self.client.get('/rest/files/1') + response = self.client.get("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - response = self.client.get('/rest/files/1?removed=true') + response = self.client.get("/rest/files/1?removed=true") self.assertEqual(response.status_code, status.HTTP_200_OK) def test_removed_parameter_gets_correct_amount_of_objects(self): - path = '/rest/datasets' + path = "/rest/datasets" objects = CatalogRecord.objects.all().values() - results = self.client.get('{0}?no_pagination&removed=false'.format(path)).json() + results = self.client.get("{0}?no_pagination&removed=false".format(path)).json() initial_amt = len(results) - results = self.client.get('{0}?no_pagination&removed=true'.format(path)).json() - self.assertEqual(len(results), 0, "Without removed objects remove=true should return 0 results") + results = self.client.get("{0}?no_pagination&removed=true".format(path)).json() + self.assertEqual( + len(results), + 0, + "Without removed objects remove=true should return 0 results", + ) self._use_http_authorization() amt_to_delete = 2 for i in range(amt_to_delete): - response = self.client.delete('{0}/{1}'.format(path, objects[i]['id'])) + response = self.client.delete("{0}/{1}".format(path, objects[i]["id"])) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - results = self.client.get('{0}?no_pagination&removed=false'.format(path)).json() - self.assertEqual(len(results), initial_amt - amt_to_delete, "Non-removed object amount is incorrect") + results = self.client.get("{0}?no_pagination&removed=false".format(path)).json() + self.assertEqual( + len(results), + initial_amt - amt_to_delete, + "Non-removed object amount is incorrect", + ) - results = self.client.get('{0}?no_pagination&removed=true'.format(path)).json() + results = self.client.get("{0}?no_pagination&removed=true".format(path)).json() self.assertEqual(len(results), amt_to_delete, "Removed object amount is incorrect") @@ -77,41 +85,59 @@ class ApiReadPaginationTests(CatalogRecordApiReadCommon): """ def test_read_catalog_record_list_pagination_1(self): - response = self.client.get('/rest/datasets?limit=2&offset=0') + response = self.client.get("/rest/datasets?limit=2&offset=0") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 2, 'There should have been exactly two results') - self.assertEqual(response.data['results'][0]['id'], 1, 'Id of first result should have been 1') + self.assertEqual( + len(response.data["results"]), + 2, + "There should have been exactly two results", + ) + self.assertEqual( + response.data["results"][0]["id"], + 1, + "Id of first result should have been 1", + ) def test_read_catalog_record_list_pagination_2(self): - response = self.client.get('/rest/datasets?limit=2&offset=2') + response = self.client.get("/rest/datasets?limit=2&offset=2") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 2, 'There should have been exactly two results') - self.assertEqual(response.data['results'][0]['id'], 3, 'Id of first result should have been 3') + self.assertEqual( + len(response.data["results"]), + 2, + "There should have been exactly two results", + ) + self.assertEqual( + response.data["results"][0]["id"], + 3, + "Id of first result should have been 3", + ) def test_disable_pagination(self): - response = self.client.get('/rest/datasets?no_pagination=true') + response = self.client.get("/rest/datasets?no_pagination=true") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('next' not in response.data, True) - self.assertEqual('results' not in response.data, True) + self.assertEqual("next" not in response.data, True) + self.assertEqual("results" not in response.data, True) def test_pagination_ordering(self): limit = 5 - for order in ('preservation_state', '-preservation_state'): + for order in ("preservation_state", "-preservation_state"): # vary offset from 0 to 20, in increments of 5 for offset in range(0, 20, 5): - response = self.client.get(f'/rest/datasets?limit={limit}&offset={offset}&ordering={order}') + response = self.client.get( + f"/rest/datasets?limit={limit}&offset={offset}&ordering={order}" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - from_api = [cr['preservation_state'] for cr in response.data['results']] + from_api = [cr["preservation_state"] for cr in response.data["results"]] from_db = [ - r for r in CatalogRecord.objects - .filter() + r + for r in CatalogRecord.objects.filter() .order_by(order) - .values_list('preservation_state', flat=True)[offset:offset + limit] + .values_list("preservation_state", flat=True)[offset : offset + limit] ] self.assertEqual(from_api, from_db) @@ -130,33 +156,35 @@ class ApiReadHTTPHeaderTests(CatalogRecordApiReadCommon): def test_get_with_if_modified_since_header_ok(self): cr = CatalogRecord.objects.get(pk=self.pk) date_modified = cr.date_modified - date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz('GMT')) + date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz("GMT")) - if_modified_since_header_value = date_modified_in_gmt.strftime('%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/datasets/%s' % self.identifier, **headers) + if_modified_since_header_value = date_modified_in_gmt.strftime("%a, %d %b %Y %H:%M:%S GMT") + headers = {"HTTP_IF_MODIFIED_SINCE": if_modified_since_header_value} + response = self.client.get("/rest/datasets/%s" % self.identifier, **headers) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) if_modified_since_header_value = (date_modified_in_gmt + timedelta(seconds=1)).strftime( - '%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/datasets/%s' % self.identifier, **headers) + "%a, %d %b %Y %H:%M:%S GMT" + ) + headers = {"HTTP_IF_MODIFIED_SINCE": if_modified_since_header_value} + response = self.client.get("/rest/datasets/%s" % self.identifier, **headers) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) if_modified_since_header_value = (date_modified_in_gmt - timedelta(seconds=1)).strftime( - '%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/datasets/%s' % self.identifier, **headers) + "%a, %d %b %Y %H:%M:%S GMT" + ) + headers = {"HTTP_IF_MODIFIED_SINCE": if_modified_since_header_value} + response = self.client.get("/rest/datasets/%s" % self.identifier, **headers) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_get_with_if_modified_since_header_syntax_error(self): cr = CatalogRecord.objects.get(pk=self.pk) date_modified = cr.date_modified - date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz('GMT')) + date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz("GMT")) - if_modified_since_header_value = date_modified_in_gmt.strftime('%a, %d %b %Y %H:%M:%S UTC') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/datasets/%s' % self.identifier, **headers) + if_modified_since_header_value = date_modified_in_gmt.strftime("%a, %d %b %Y %H:%M:%S UTC") + headers = {"HTTP_IF_MODIFIED_SINCE": if_modified_since_header_value} + response = self.client.get("/rest/datasets/%s" % self.identifier, **headers) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # @@ -168,41 +196,43 @@ def test_get_with_if_modified_since_header_syntax_error(self): def test_list_get_with_if_modified_since_header_ok(self): cr = CatalogRecord.objects.get(pk=self.pk) date_modified = cr.date_modified - date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz('GMT')) + date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz("GMT")) - if_modified_since_header_value = date_modified_in_gmt.strftime('%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/datasets?limit=100', **headers) + if_modified_since_header_value = date_modified_in_gmt.strftime("%a, %d %b %Y %H:%M:%S GMT") + headers = {"HTTP_IF_MODIFIED_SINCE": if_modified_since_header_value} + response = self.client.get("/rest/datasets?limit=100", **headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(len(response.data.get('results')) == 6) + self.assertTrue(len(response.data.get("results")) == 6) if_modified_since_header_value = (date_modified_in_gmt + timedelta(seconds=1)).strftime( - '%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/datasets?limit=100', **headers) + "%a, %d %b %Y %H:%M:%S GMT" + ) + headers = {"HTTP_IF_MODIFIED_SINCE": if_modified_since_header_value} + response = self.client.get("/rest/datasets?limit=100", **headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(len(response.data.get('results')) == 6) + self.assertTrue(len(response.data.get("results")) == 6) # The asserts below may brake if the date_modified timestamps or the amount of test data objects are altered # in the test data if_modified_since_header_value = (date_modified_in_gmt - timedelta(seconds=1)).strftime( - '%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/datasets?limit=100', **headers) + "%a, %d %b %Y %H:%M:%S GMT" + ) + headers = {"HTTP_IF_MODIFIED_SINCE": if_modified_since_header_value} + response = self.client.get("/rest/datasets?limit=100", **headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(len(response.data.get('results')) > 6) - self.assertTrue(len(response.data.get('results')) == 28) + self.assertTrue(len(response.data.get("results")) > 6) + self.assertTrue(len(response.data.get("results")) == 28) # should also work with records that have been recently created, and date_modified is empty cr.date_created = date_modified cr.date_modified = None cr.force_save() - response = self.client.get('/rest/datasets?limit=100', **headers) + response = self.client.get("/rest/datasets?limit=100", **headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(len(response.data.get('results')) > 6) - self.assertTrue(len(response.data.get('results')) == 28) + self.assertTrue(len(response.data.get("results")) > 6) + self.assertTrue(len(response.data.get("results")) == 28) class ApiReadQueryParamTests(CatalogRecordApiReadCommon): @@ -215,28 +245,28 @@ def test_return_requested_fields_only(self): """ While the param ?fields works with write operations too, the primary use case is when GETting. """ - response = self.client.get('/rest/datasets?fields=identifier') + response = self.client.get("/rest/datasets?fields=identifier") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('identifier' in response.data['results'][0], True) - self.assertEqual(len(response.data['results'][0].keys()), 1) - self.assertEqual(len(response.data['results'][1].keys()), 1) + self.assertEqual("identifier" in response.data["results"][0], True) + self.assertEqual(len(response.data["results"][0].keys()), 1) + self.assertEqual(len(response.data["results"][1].keys()), 1) - response = self.client.get('/rest/datasets/1?fields=identifier') + response = self.client.get("/rest/datasets/1?fields=identifier") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('identifier' in response.data, True) + self.assertEqual("identifier" in response.data, True) self.assertEqual(len(response.data.keys()), 1) - response = self.client.get('/rest/datasets/1?fields=identifier,data_catalog') - self.assertEqual('identifier' in response.data, True) - self.assertEqual('data_catalog' in response.data, True) + response = self.client.get("/rest/datasets/1?fields=identifier,data_catalog") + self.assertEqual("identifier" in response.data, True) + self.assertEqual("data_catalog" in response.data, True) self.assertEqual(len(response.data.keys()), 2) - response = self.client.get('/rest/datasets/1?fields=not_found') + response = self.client.get("/rest/datasets/1?fields=not_found") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # Anonymous user using fields parameter and not including research_dataset should not cause crashing self.client._credentials = {} - response = self.client.get('/rest/datasets/1?fields=identifier') + response = self.client.get("/rest/datasets/1?fields=identifier") self.assertEqual(response.status_code, status.HTTP_200_OK) def test_checksum_field_for_file(self): @@ -244,20 +274,23 @@ def test_checksum_field_for_file(self): Check that checksum field works correctly """ - self._use_http_authorization('metax') - response = self.client.get('/rest/files/1?fields=checksum') + self._use_http_authorization("metax") + response = self.client.get("/rest/files/1?fields=checksum") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data.get('checksum'), 'Checksum JSON should be returned') - self.assertTrue(response.data['checksum'].get('algorithm')) - self.assertTrue(response.data['checksum'].get('checked')) - self.assertTrue(response.data['checksum'].get('value')) + self.assertTrue(response.data.get("checksum"), "Checksum JSON should be returned") + self.assertTrue(response.data["checksum"].get("algorithm")) + self.assertTrue(response.data["checksum"].get("checked")) + self.assertTrue(response.data["checksum"].get("value")) - response = self.client.get('/rest/files/1?fields=checksum:value') + response = self.client.get("/rest/files/1?fields=checksum:value") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data.get('checksum'), 'Checksum JSON should be returned') - self.assertTrue(response.data['checksum'].get('value')) - self.assertFalse(response.data['checksum'].get('algorithm')) + self.assertTrue(response.data.get("checksum"), "Checksum JSON should be returned") + self.assertTrue(response.data["checksum"].get("value")) + self.assertFalse(response.data["checksum"].get("algorithm")) - response = self.client.get('/rest/files/1?fields=checksum:badvalue') + response = self.client.get("/rest/files/1?fields=checksum:badvalue") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('is not part of' in response.data['detail'][0], 'Should complain about field not found') + self.assertTrue( + "is not part of" in response.data["detail"][0], + "Should complain about field not found", + ) diff --git a/src/metax_api/tests/api/rest/base/views/common/write.py b/src/metax_api/tests/api/rest/base/views/common/write.py index be5fc179..7a630879 100755 --- a/src/metax_api/tests/api/rest/base/views/common/write.py +++ b/src/metax_api/tests/api/rest/base/views/common/write.py @@ -27,28 +27,30 @@ class ApiWriteCommon(APITestCase, TestClassUtils): - def setUp(self): - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) self.test_new_data = self._get_new_test_data() self._use_http_authorization() def _get_new_test_data(self): - record_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=0) - record_from_test_data.update({ - "data_catalog": 1, - }) - record_from_test_data['research_dataset'].update({ - "preferred_identifier": None, - }) - record_from_test_data.pop('id', None) - record_from_test_data.pop('identifier', None) - record_from_test_data.pop('contract', None) + record_from_test_data = self._get_object_from_test_data("catalogrecord", requested_index=0) + record_from_test_data.update( + { + "data_catalog": 1, + } + ) + record_from_test_data["research_dataset"].update( + { + "preferred_identifier": None, + } + ) + record_from_test_data.pop("id", None) + record_from_test_data.pop("identifier", None) + record_from_test_data.pop("contract", None) return record_from_test_data class ApiWriteCommonFieldsTests(ApiWriteCommon): - def test_certain_create_fields_are_read_only_after_create(self): """ The following fields should be read-only after initial creation of a resource: @@ -56,61 +58,64 @@ def test_certain_create_fields_are_read_only_after_create(self): - user_created - service_created """ - response = self.client.post('/rest/datasets', self.test_new_data, format="json") + response = self.client.post("/rest/datasets", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # some of the fields could be empty in test data. that is fine tho, the point is that # they should not change later. - orig_date_created = response.data.get('date_created', None) - orig_user_created = response.data.get('user_created', None) - orig_service_created = response.data.get('service_created', None) + orig_date_created = response.data.get("date_created", None) + orig_user_created = response.data.get("user_created", None) + orig_service_created = response.data.get("service_created", None) altered = response.data - altered['date_created'] = altered['date_created'].replace('2017', '2010') - altered['user_created'] = 'changed' - altered['service_created'] = 'changed' + altered["date_created"] = altered["date_created"].replace("2017", "2010") + altered["user_created"] = "changed" + altered["service_created"] = "changed" - response = self.client.put('/rest/datasets/%d' % altered['id'], altered, format="json") + response = self.client.put("/rest/datasets/%d" % altered["id"], altered, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/datasets/%d' % altered['id'], format="json") + response = self.client.get("/rest/datasets/%d" % altered["id"], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(orig_date_created, response.data.get('date_created', None)) - self.assertEqual(orig_user_created, response.data.get('user_created', None)) - self.assertEqual(orig_service_created, response.data.get('service_created', None)) + self.assertEqual(orig_date_created, response.data.get("date_created", None)) + self.assertEqual(orig_user_created, response.data.get("user_created", None)) + self.assertEqual(orig_service_created, response.data.get("service_created", None)) def test_deletion_sets_removed_true_and_sets_value_for_date_removed(self): - response = self.client.post('/rest/datasets', self.test_new_data, format="json") + response = self.client.post("/rest/datasets", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] - response = self.client.delete('/rest/datasets/%d' % cr_id) + cr_id = response.data["id"] + response = self.client.delete("/rest/datasets/%d" % cr_id) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) # Verify date_removed got set - response = self.client.get('/rest/datasets/%d?removed' % cr_id) + response = self.client.get("/rest/datasets/%d?removed" % cr_id) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data['removed'] is True) - self.assertTrue(response.data.get('date_removed', '').startswith('2')) + self.assertTrue(response.data["removed"] is True) + self.assertTrue(response.data.get("date_removed", "").startswith("2")) def test_updating_sets_removed_false_and_empties_date_removed(self): - response = self.client.post('/rest/datasets', self.test_new_data, format="json") + response = self.client.post("/rest/datasets", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] - response = self.client.delete('/rest/datasets/%d' % cr_id) + cr_id = response.data["id"] + response = self.client.delete("/rest/datasets/%d" % cr_id) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - rd = self.client.get('/rest/datasets/%d?removed' % cr_id).data - rd_date_rem = rd['date_removed'] - sleep(1) # ensure that next request happens with different timestamp - response = self.client.put('/rest/datasets/%d?removed' % cr_id, rd, format="json") + rd = self.client.get("/rest/datasets/%d?removed" % cr_id).data + rd_date_rem = rd["date_removed"] + sleep(1) # ensure that next request happens with different timestamp + response = self.client.put("/rest/datasets/%d?removed" % cr_id, rd, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/datasets/%d' % cr_id) + response = self.client.get("/rest/datasets/%d" % cr_id) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data['removed'] is False) - self.assertTrue(response.data.get('date_removed') is None) - self.assertTrue(response.data.get('date_modified') != rd_date_rem, 'date_modified should be updated') + self.assertTrue(response.data["removed"] is False) + self.assertTrue(response.data.get("date_removed") is None) + self.assertTrue( + response.data.get("date_modified") != rd_date_rem, + "date_modified should be updated", + ) class ApiWriteHTTPHeaderTests(CatalogRecordApiWriteCommon): @@ -120,47 +125,53 @@ class ApiWriteHTTPHeaderTests(CatalogRecordApiWriteCommon): # def test_update_with_if_unmodified_since_header_ok(self): - cr = self.client.get('/rest/datasets/1').data - cr['preservation_description'] = 'damn this is good coffee' + cr = self.client.get("/rest/datasets/1").data + cr["preservation_description"] = "damn this is good coffee" cr_obj = CatalogRecord.objects.get(pk=1) - headers = {'HTTP_IF_UNMODIFIED_SINCE': cr_obj.date_modified.strftime('%a, %d %b %Y %H:%M:%S GMT')} + headers = { + "HTTP_IF_UNMODIFIED_SINCE": cr_obj.date_modified.strftime("%a, %d %b %Y %H:%M:%S GMT") + } - response = self.client.put('/rest/datasets/1', cr, format="json", **headers) + response = self.client.put("/rest/datasets/1", cr, format="json", **headers) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) def test_update_with_if_unmodified_since_header_precondition_failed_error(self): - cr = self.client.get('/rest/datasets/1').data - cr['preservation_description'] = 'the owls are not what they seem' + cr = self.client.get("/rest/datasets/1").data + cr["preservation_description"] = "the owls are not what they seem" - headers = {'HTTP_IF_UNMODIFIED_SINCE': 'Wed, 23 Sep 2009 22:15:29 GMT'} + headers = {"HTTP_IF_UNMODIFIED_SINCE": "Wed, 23 Sep 2009 22:15:29 GMT"} - response = self.client.put('/rest/datasets/1', cr, format="json", **headers) - self.assertEqual(response.status_code, 412, 'http status should be 412 = precondition failed') + response = self.client.put("/rest/datasets/1", cr, format="json", **headers) + self.assertEqual( + response.status_code, 412, "http status should be 412 = precondition failed" + ) def test_update_with_if_unmodified_since_header_syntax_error(self): - cr = self.client.get('/rest/datasets/1').data - cr['preservation_description'] = 'the owls are not what they seem' + cr = self.client.get("/rest/datasets/1").data + cr["preservation_description"] = "the owls are not what they seem" cr_obj = CatalogRecord.objects.get(pk=1) - headers = {'HTTP_IF_UNMODIFIED_SINCE': cr_obj.date_modified.strftime('%a, %d %b %Y %H:%M:%S UTC')} + headers = { + "HTTP_IF_UNMODIFIED_SINCE": cr_obj.date_modified.strftime("%a, %d %b %Y %H:%M:%S UTC") + } - response = self.client.put('/rest/datasets/1', cr, format="json", **headers) - self.assertEqual(response.status_code, 400, 'http status should be 400') + response = self.client.put("/rest/datasets/1", cr, format="json", **headers) + self.assertEqual(response.status_code, 400, "http status should be 400") # # header if-unmodified-since tests, list # def test_update_list_with_if_unmodified_since_header_ok(self): - data_1 = self.client.get('/rest/datasets/1', format="json").data - data_2 = self.client.get('/rest/datasets/2', format="json").data + data_1 = self.client.get("/rest/datasets/1", format="json").data + data_2 = self.client.get("/rest/datasets/2", format="json").data - data_1['preservation_description'] = 'damn this is good coffee' - data_2['preservation_description'] = 'damn this is good coffee also' + data_1["preservation_description"] = "damn this is good coffee" + data_2["preservation_description"] = "damn this is good coffee also" - headers = {'HTTP_IF_UNMODIFIED_SINCE': 'value is not checked'} - response = self.client.put('/rest/datasets', [data_1, data_2], format="json", **headers) + headers = {"HTTP_IF_UNMODIFIED_SINCE": "value is not checked"} + response = self.client.put("/rest/datasets", [data_1, data_2], format="json", **headers) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) @@ -168,36 +179,46 @@ def test_update_list_with_if_unmodified_since_header_error_1(self): """ One resource being updated was updated in the meantime, resulting in an error """ - data_1 = self.client.get('/rest/datasets/1', format="json").data - data_2 = self.client.get('/rest/datasets/2', format="json").data + data_1 = self.client.get("/rest/datasets/1", format="json").data + data_2 = self.client.get("/rest/datasets/2", format="json").data - data_1['preservation_description'] = 'damn this is good coffee' + data_1["preservation_description"] = "damn this is good coffee" # should result in error for this record - data_2['date_modified'] = '2002-01-01T10:10:10Z' - - headers = {'HTTP_IF_UNMODIFIED_SINCE': 'value is not checked'} - response = self.client.put('/rest/datasets', [data_1, data_2], format="json", **headers) - self.assertEqual(len(response.data['failed']) == 1, True, 'there should be only one failed update') - self.assertEqual('modified' in response.data['failed'][0]['errors']['detail'][0], True, - 'error should indicate resource has been modified') + data_2["date_modified"] = "2002-01-01T10:10:10Z" + + headers = {"HTTP_IF_UNMODIFIED_SINCE": "value is not checked"} + response = self.client.put("/rest/datasets", [data_1, data_2], format="json", **headers) + self.assertEqual( + len(response.data["failed"]) == 1, + True, + "there should be only one failed update", + ) + self.assertEqual( + "modified" in response.data["failed"][0]["errors"]["detail"][0], + True, + "error should indicate resource has been modified", + ) def test_update_list_with_if_unmodified_since_header_error_2(self): """ Field date_modified is missing, while if-modified-since header is set, resulting in an error. """ - data_1 = self.client.get('/rest/datasets/1', format="json").data - data_2 = self.client.get('/rest/datasets/2', format="json").data + data_1 = self.client.get("/rest/datasets/1", format="json").data + data_2 = self.client.get("/rest/datasets/2", format="json").data - data_1['preservation_description'] = 'damn this is good coffee' + data_1["preservation_description"] = "damn this is good coffee" # should result in error for this record - data_2.pop('date_modified') + data_2.pop("date_modified") - headers = {'HTTP_IF_UNMODIFIED_SINCE': 'value is not checked'} - response = self.client.patch('/rest/datasets', [data_1, data_2], format="json", **headers) - self.assertEqual('required' in response.data['failed'][0]['errors']['detail'][0], True, - 'error should be about field date_modified is required') + headers = {"HTTP_IF_UNMODIFIED_SINCE": "value is not checked"} + response = self.client.patch("/rest/datasets", [data_1, data_2], format="json", **headers) + self.assertEqual( + "required" in response.data["failed"][0]["errors"]["detail"][0], + True, + "error should be about field date_modified is required", + ) def test_update_list_with_if_unmodified_since_header_error_3(self): """ @@ -205,17 +226,20 @@ def test_update_list_with_if_unmodified_since_header_error_3(self): is an accepted value. The end result should be that the resource has been modified, since the server version has a timestamp set in date_modified. """ - data_1 = self.client.get('/rest/datasets/1', format="json").data - data_2 = self.client.get('/rest/datasets/2', format="json").data + data_1 = self.client.get("/rest/datasets/1", format="json").data + data_2 = self.client.get("/rest/datasets/2", format="json").data - data_1['preservation_description'] = 'damn this is good coffee' - data_2['preservation_description'] = 'damn this is good coffee also' - data_2['date_modified'] = None + data_1["preservation_description"] = "damn this is good coffee" + data_2["preservation_description"] = "damn this is good coffee also" + data_2["date_modified"] = None - headers = {'HTTP_IF_UNMODIFIED_SINCE': 'value is not checked'} - response = self.client.put('/rest/datasets', [data_1, data_2], format="json", **headers) - self.assertEqual('modified' in response.data['failed'][0]['errors']['detail'][0], True, - 'error should indicate resource has been modified') + headers = {"HTTP_IF_UNMODIFIED_SINCE": "value is not checked"} + response = self.client.put("/rest/datasets", [data_1, data_2], format="json", **headers) + self.assertEqual( + "modified" in response.data["failed"][0]["errors"]["detail"][0], + True, + "error should indicate resource has been modified", + ) class ApiWriteAtomicBulkOperations(CatalogRecordApiWriteCommon): @@ -226,47 +250,55 @@ class ApiWriteAtomicBulkOperations(CatalogRecordApiWriteCommon): """ def test_atomic_create(self): - cr = self.client.get('/rest/datasets/1', format="json").data - cr.pop('id') - cr.pop('identifier') - cr['research_dataset'].pop('metadata_version_identifier') - cr['research_dataset'].pop('preferred_identifier') + cr = self.client.get("/rest/datasets/1", format="json").data + cr.pop("id") + cr.pop("identifier") + cr["research_dataset"].pop("metadata_version_identifier") + cr["research_dataset"].pop("preferred_identifier") cr2 = deepcopy(cr) cr3 = deepcopy(cr) - cr3.pop('data_catalog') # causes error + cr3.pop("data_catalog") # causes error record_count_before = CatalogRecord.objects.all().count() - response = self.client.post('/rest/datasets?atomic=true', [cr, cr2, cr3], format="json") + response = self.client.post("/rest/datasets?atomic=true", [cr, cr2, cr3], format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.content) - self.assertEqual(len(response.data['success']) == 0, True, response.data) - self.assertEqual(len(response.data['failed']) == 1, True, response.data) - self.assertEqual('detail' in response.data, True, response.data) - self.assertEqual('atomic' in response.data['detail'][0], True, response.data) - self.assertEqual(record_count_before, CatalogRecord.objects.all().count(), 'shouldnt create new records') + self.assertEqual(len(response.data["success"]) == 0, True, response.data) + self.assertEqual(len(response.data["failed"]) == 1, True, response.data) + self.assertEqual("detail" in response.data, True, response.data) + self.assertEqual("atomic" in response.data["detail"][0], True, response.data) + self.assertEqual( + record_count_before, + CatalogRecord.objects.all().count(), + "shouldnt create new records", + ) def test_atomic_update(self): - cr = self.client.get('/rest/datasets/1', format="json").data - cr2 = self.client.get('/rest/datasets/2', format="json").data - cr3 = self.client.get('/rest/datasets/3', format="json").data - cr['research_dataset']['title']['en'] = 'updated' - cr2['research_dataset']['title']['en'] = 'updated' - cr3.pop('data_catalog') # causes error + cr = self.client.get("/rest/datasets/1", format="json").data + cr2 = self.client.get("/rest/datasets/2", format="json").data + cr3 = self.client.get("/rest/datasets/3", format="json").data + cr["research_dataset"]["title"]["en"] = "updated" + cr2["research_dataset"]["title"]["en"] = "updated" + cr3.pop("data_catalog") # causes error record_count_before = CatalogRecord.objects.all().count() - response = self.client.put('/rest/datasets?atomic=true', [cr, cr2, cr3], format="json") + response = self.client.put("/rest/datasets?atomic=true", [cr, cr2, cr3], format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(len(response.data['success']) == 0, True) - self.assertEqual(len(response.data['failed']) == 1, True) - self.assertEqual('atomic' in response.data['detail'][0], True) - self.assertEqual(record_count_before, CatalogRecord.objects.all().count(), 'shouldnt create new versions') + self.assertEqual(len(response.data["success"]) == 0, True) + self.assertEqual(len(response.data["failed"]) == 1, True) + self.assertEqual("atomic" in response.data["detail"][0], True) + self.assertEqual( + record_count_before, + CatalogRecord.objects.all().count(), + "shouldnt create new versions", + ) - cr = self.client.get('/rest/datasets/1', format="json").data - cr2 = self.client.get('/rest/datasets/2', format="json").data - self.assertEqual(cr['research_dataset']['title']['en'] == 'updated', False) - self.assertEqual(cr2['research_dataset']['title']['en'] == 'updated', False) + cr = self.client.get("/rest/datasets/1", format="json").data + cr2 = self.client.get("/rest/datasets/2", format="json").data + self.assertEqual(cr["research_dataset"]["title"]["en"] == "updated", False) + self.assertEqual(cr2["research_dataset"]["title"]["en"] == "updated", False) class ApiWriteQueryParamTests(ApiWriteCommon): @@ -280,11 +312,15 @@ def test_dryrun(self): Ensure query parameter ?dryrun=true returns same result as they normally would, but changes made during the request do not get saved in the db. """ - response = self.client.post('/rest/datasets?dryrun=true', self.test_new_data, format="json") + response = self.client.post("/rest/datasets?dryrun=true", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('id' in response.data, True) - found = CatalogRecord.objects.filter(pk=response.data['id']).exists() - self.assertEqual(found, False, 'record should not get truly created when using parameter dryrun') + self.assertEqual("id" in response.data, True) + found = CatalogRecord.objects.filter(pk=response.data["id"]).exists() + self.assertEqual( + found, + False, + "record should not get truly created when using parameter dryrun", + ) class ApiWriteCommonOperations(ApiWriteCommon): @@ -293,10 +329,10 @@ class ApiWriteCommonOperations(ApiWriteCommon): """ def test_create_file_with_empty_body_fails(self): - response = self.client.post('/rest/datasets') + response = self.client.post("/rest/datasets") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('Request body is required' in response.data['detail'][0]) + self.assertTrue("Request body is required" in response.data["detail"][0]) - response = self.client.post('/rest/files') + response = self.client.post("/rest/files") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('Request body is required' in response.data['detail'][0]) + self.assertTrue("Request body is required" in response.data["detail"][0]) diff --git a/src/metax_api/tests/api/rest/base/views/contracts/contracts.py b/src/metax_api/tests/api/rest/base/views/contracts/contracts.py index 90babef0..b79f55e0 100755 --- a/src/metax_api/tests/api/rest/base/views/contracts/contracts.py +++ b/src/metax_api/tests/api/rest/base/views/contracts/contracts.py @@ -19,29 +19,29 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(ContractApiReadTestV1, cls).setUpClass() def setUp(self): - contract_from_test_data = self._get_object_from_test_data('contract', requested_index=0) - self.pk = contract_from_test_data['id'] - self.identifier = contract_from_test_data['contract_json']['identifier'] + contract_from_test_data = self._get_object_from_test_data("contract", requested_index=0) + self.pk = contract_from_test_data["id"] + self.identifier = contract_from_test_data["contract_json"]["identifier"] self._use_http_authorization() def test_read_contract_list(self): - response = self.client.get('/rest/datasets') + response = self.client.get("/rest/datasets") self.assertEqual(response.status_code, status.HTTP_200_OK) def test_read_contract_details_by_pk(self): - response = self.client.get('/rest/contracts/%s' % self.pk) + response = self.client.get("/rest/contracts/%s" % self.pk) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_read_contract_details_by_identifier(self): - response = self.client.get('/rest/contracts/%s' % self.identifier) + response = self.client.get("/rest/contracts/%s" % self.identifier) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_read_contract_details_not_found(self): - response = self.client.get('/rest/contracts/shouldnotexist') + response = self.client.get("/rest/contracts/shouldnotexist") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) @@ -50,10 +50,10 @@ def setUp(self): """ Reloaded for every test case """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) self._use_http_authorization() - contract_from_test_data = self._get_object_from_test_data('contract') - self.pk = contract_from_test_data['id'] + contract_from_test_data = self._get_object_from_test_data("contract") + self.pk = contract_from_test_data["id"] """ New data that is sent to the server for POST, PUT, PATCH requests. Modified @@ -64,32 +64,38 @@ def setUp(self): self._use_http_authorization() def test_create_contract_with_existing_identifier(self): - self.test_new_data['pk'] = self.pk - response = self.client.post('/rest/contracts/', self.test_new_data, format="json") + self.test_new_data["pk"] = self.pk + response = self.client.post("/rest/contracts/", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - response = self.client.post('/rest/contracts/', self.test_new_data, format="json") + response = self.client.post("/rest/contracts/", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('already exists' in response.data['contract_json'][0], - 'Error regarding dublicated identifier') + self.assertTrue( + "already exists" in response.data["contract_json"][0], + "Error regarding dublicated identifier", + ) def test_update_contract(self): - self.test_new_data['pk'] = self.pk - response = self.client.put('/rest/contracts/%s' % self.pk, self.test_new_data, format="json") + self.test_new_data["pk"] = self.pk + response = self.client.put( + "/rest/contracts/%s" % self.pk, self.test_new_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) def test_update_contract_not_found(self): - response = self.client.put('/rest/contracts/doesnotexist', self.test_new_data, format="json") + response = self.client.put( + "/rest/contracts/doesnotexist", self.test_new_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_add_catalog_record_to_contract(self): - new_catalog_record = self.client.get('/rest/datasets/1', format="json").data - new_catalog_record.pop('id') - new_catalog_record.pop('identifier') - new_catalog_record['research_dataset'].pop('preferred_identifier') - new_catalog_record['contract'] = self.pk + new_catalog_record = self.client.get("/rest/datasets/1", format="json").data + new_catalog_record.pop("id") + new_catalog_record.pop("identifier") + new_catalog_record["research_dataset"].pop("preferred_identifier") + new_catalog_record["contract"] = self.pk - response = self.client.post('/rest/datasets', new_catalog_record, format="json") + response = self.client.post("/rest/datasets", new_catalog_record, format="json") created_catalog_record = response.data try: @@ -97,21 +103,26 @@ def test_add_catalog_record_to_contract(self): except Exception: print(response.data) raise - self.assertEqual('research_dataset' in created_catalog_record.keys(), True) - self.assertEqual(created_catalog_record['contract']['id'], self.pk) + self.assertEqual("research_dataset" in created_catalog_record.keys(), True) + self.assertEqual(created_catalog_record["contract"]["id"], self.pk) contract = Contract.objects.get(pk=self.pk) try: - contract.records.get(pk=response.data['id']) + contract.records.get(pk=response.data["id"]) except CatalogRecord.DoesNotExist: - raise Exception('The added CatalogRecord should appear in the relation contract.records') + raise Exception( + "The added CatalogRecord should appear in the relation contract.records" + ) - response = self.client.get('/rest/contracts/%d/datasets' % self.pk) - self.assertIn(created_catalog_record['id'], [cr['id'] for cr in response.data], - 'The added CatalogRecord should appear in the results of /contracts/id/datasets') + response = self.client.get("/rest/contracts/%d/datasets" % self.pk) + self.assertIn( + created_catalog_record["id"], + [cr["id"] for cr in response.data], + "The added CatalogRecord should appear in the results of /contracts/id/datasets", + ) def test_delete_contract(self): - url = '/rest/contracts/%s' % self.pk + url = "/rest/contracts/%s" % self.pk response = self.client.delete(url) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) response = self.client.get(url) @@ -125,43 +136,65 @@ def test_delete_contract(self): pass if deleted_contract: - raise Exception('Deleted Contract should not be retrievable from the default objects table') + raise Exception( + "Deleted Contract should not be retrievable from the default objects table" + ) try: deleted_contract = Contract.objects_unfiltered.get(pk=self.pk) except Contract.DoesNotExist: - raise Exception('Deleted contract should not be deleted from the db') - - self.assertEqual(deleted_contract.removed, True, 'Deleted contract should be marked removed in the db') - self.assertEqual(deleted_contract.date_modified, deleted_contract.date_removed, - 'date_modified should be updated') + raise Exception("Deleted contract should not be deleted from the db") + + self.assertEqual( + deleted_contract.removed, + True, + "Deleted contract should be marked removed in the db", + ) + self.assertEqual( + deleted_contract.date_modified, + deleted_contract.date_removed, + "date_modified should be updated", + ) def test_delete_contract_catalog_records_are_marked_removed(self): # add two new records to contract new_catalog_record = self._get_new_catalog_record_test_data() - new_catalog_record['contract'] = self.pk - self.client.post('/rest/datasets', new_catalog_record, format="json") - self.client.post('/rest/datasets', new_catalog_record, format="json") + new_catalog_record["contract"] = self.pk + self.client.post("/rest/datasets", new_catalog_record, format="json") + self.client.post("/rest/datasets", new_catalog_record, format="json") - self.client.delete('/rest/contracts/%s' % self.pk) + self.client.delete("/rest/contracts/%s" % self.pk) contract = Contract.objects_unfiltered.get(pk=self.pk) - related_crs = contract.records(manager='objects_unfiltered').all() - response_get_1 = self.client.get('/rest/datasets/%d' % related_crs[0].id) - self.assertEqual(response_get_1.status_code, status.HTTP_404_NOT_FOUND, - 'CatalogRecords of deleted contracts should not be retrievable through the api') - response_get_2 = self.client.get('/rest/datasets/%d' % related_crs[1].id) - self.assertEqual(response_get_2.status_code, status.HTTP_404_NOT_FOUND, - 'CatalogRecords of deleted contracts should not be retrievable through the api') + related_crs = contract.records(manager="objects_unfiltered").all() + response_get_1 = self.client.get("/rest/datasets/%d" % related_crs[0].id) + self.assertEqual( + response_get_1.status_code, + status.HTTP_404_NOT_FOUND, + "CatalogRecords of deleted contracts should not be retrievable through the api", + ) + response_get_2 = self.client.get("/rest/datasets/%d" % related_crs[1].id) + self.assertEqual( + response_get_2.status_code, + status.HTTP_404_NOT_FOUND, + "CatalogRecords of deleted contracts should not be retrievable through the api", + ) for cr in related_crs: - self.assertEqual(cr.removed, True, 'Related CatalogRecord objects should be marked as removed') + self.assertEqual( + cr.removed, + True, + "Related CatalogRecord objects should be marked as removed", + ) def test_deleted_catalog_record_is_not_listed_in_contract_datasets_api(self): deleted_id = 1 - self.client.delete('/rest/datasets/%d' % deleted_id) - response = self.client.get('/rest/contracts/%d/datasets' % self.pk) - self.assertNotIn(deleted_id, [cr['id'] for cr in response.data], - 'The deleted CatalogRecord should not appear in the results of /contracts/id/datasets') + self.client.delete("/rest/datasets/%d" % deleted_id) + response = self.client.get("/rest/contracts/%d/datasets" % self.pk) + self.assertNotIn( + deleted_id, + [cr["id"] for cr in response.data], + "The deleted CatalogRecord should not appear in the results of /contracts/id/datasets", + ) def _get_new_test_data(self): return { @@ -172,22 +205,19 @@ def _get_new_test_data(self): "created": "2014-01-17T08:19:58Z", "modified": "2014-01-17T08:19:58Z", "description": "Description of unknown length", - "contact": [{ - "name": "Contact Name", - "phone": "+358501231234", - "email": "contact.email@csc.fi" - }], + "contact": [ + { + "name": "Contact Name", + "phone": "+358501231234", + "email": "contact.email@csc.fi", + } + ], "organization": { "organization_identifier": "1234567abc", - "name": "Mysterious organization" + "name": "Mysterious organization", }, - "related_service": [{ - "identifier": "local:service:id", - "name": "Name of Service" - }], - "validity": { - "start_date": "2014-01-17" - } + "related_service": [{"identifier": "local:service:id", "name": "Name of Service"}], + "validity": {"start_date": "2014-01-17"}, } } @@ -200,57 +230,56 @@ def _get_second_new_test_data(self): "created": "2014-01-17T08:19:58Z", "modified": "2014-01-17T08:19:58Z", "description": "Description of unknown length", - "contact": [{ - "name": "Contact Name", - "phone": "+358501231234", - "email": "contact.email@csc.fi" - }], + "contact": [ + { + "name": "Contact Name", + "phone": "+358501231234", + "email": "contact.email@csc.fi", + } + ], "organization": { "organization_identifier": "1234567abc", - "name": "Mysterious organization" + "name": "Mysterious organization", }, - "related_service": [{ - "identifier": "local:service:id", - "name": "Name of Service" - }], - "validity": { - "start_date": "2014-01-17" - } + "related_service": [{"identifier": "local:service:id", "name": "Name of Service"}], + "validity": {"start_date": "2014-01-17"}, } } def _get_new_catalog_record_test_data(self): - catalog_record_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=0) + catalog_record_from_test_data = self._get_object_from_test_data( + "catalogrecord", requested_index=0 + ) return { "identifier": "http://urn.fi/urn:nbn:fi:iiidentifier", - "data_catalog": self._get_object_from_test_data('datacatalog', requested_index=0), + "data_catalog": self._get_object_from_test_data("datacatalog", requested_index=0), "research_dataset": { "modified": "2014-01-17T08:19:58Z", - "version_notes": [ - "This version contains changes to x and y." + "version_notes": ["This version contains changes to x and y."], + "title": {"en": "Wonderful Title"}, + "description": [ + { + "en": "A descriptive description describing the contents of this dataset. Must be descriptive." + } ], - "title": { - "en": "Wonderful Title" - }, - "description": [{ - "en": "A descriptive description describing the contents of this dataset. Must be descriptive." - }], - "creator": [{ - "@type": "Person", - "name": "Teppo Testaaja", - "member_of": { + "creator": [ + { + "@type": "Person", + "name": "Teppo Testaaja", + "member_of": { + "@type": "Organization", + "name": {"fi": "Mysterious Organization"}, + }, + } + ], + "curator": [ + { "@type": "Organization", - "name": {"fi": "Mysterious Organization"} + "name": {"en": "Curator org", "fi": "Organisaatio"}, } - }], - "curator": [{ - "@type": "Organization", - "name": {"en": "Curator org", "fi": "Organisaatio"} - }], - "language": [{ - "identifier": "http://lexvo.org/id/iso639-3/aar" - }], + ], + "language": [{"identifier": "http://lexvo.org/id/iso639-3/aar"}], "total_files_byte_size": 1024, - "files": catalog_record_from_test_data['research_dataset']['files'] - } + "files": catalog_record_from_test_data["research_dataset"]["files"], + }, } diff --git a/src/metax_api/tests/api/rest/base/views/datacatalogs/read.py b/src/metax_api/tests/api/rest/base/views/datacatalogs/read.py index 7d44b705..c93e5987 100755 --- a/src/metax_api/tests/api/rest/base/views/datacatalogs/read.py +++ b/src/metax_api/tests/api/rest/base/views/datacatalogs/read.py @@ -18,22 +18,24 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(DataCatalogApiReadBasicTests, cls).setUpClass() def setUp(self): - data_catalog_from_test_data = self._get_object_from_test_data('datacatalog', requested_index=0) + data_catalog_from_test_data = self._get_object_from_test_data( + "datacatalog", requested_index=0 + ) self._use_http_authorization() - self.pk = data_catalog_from_test_data['id'] - self.identifier = data_catalog_from_test_data['catalog_json']['identifier'] + self.pk = data_catalog_from_test_data["id"] + self.identifier = data_catalog_from_test_data["catalog_json"]["identifier"] def test_basic_get(self): - response = self.client.get('/rest/datacatalogs/%s' % self.identifier) + response = self.client.get("/rest/datacatalogs/%s" % self.identifier) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_allowed_read_methods(self): self.client._credentials = {} - for req in ['/rest/datacatalogs', '/rest/datacatalogs/1']: + for req in ["/rest/datacatalogs", "/rest/datacatalogs/1"]: response = self.client.get(req) self.assertEqual(response.status_code, status.HTTP_200_OK) response = self.client.head(req) diff --git a/src/metax_api/tests/api/rest/base/views/datacatalogs/write.py b/src/metax_api/tests/api/rest/base/views/datacatalogs/write.py index ae73ee98..0edacec7 100755 --- a/src/metax_api/tests/api/rest/base/views/datacatalogs/write.py +++ b/src/metax_api/tests/api/rest/base/views/datacatalogs/write.py @@ -21,55 +21,65 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(DataCatalogApiWriteCommon, cls).setUpClass() def setUp(self): - self.new_test_data = self._get_object_from_test_data('datacatalog') - self.new_test_data.pop('id') - self.new_test_data['catalog_json']['identifier'] = 'new-data-catalog' + self.new_test_data = self._get_object_from_test_data("datacatalog") + self.new_test_data.pop("id") + self.new_test_data["catalog_json"]["identifier"] = "new-data-catalog" self._use_http_authorization() class DataCatalogApiWriteBasicTests(DataCatalogApiWriteCommon): - def test_identifier_is_auto_generated(self): - response = self.client.post('/rest/datacatalogs', self.new_test_data, format="json") + response = self.client.post("/rest/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertNotEqual(response.data['catalog_json'].get('identifier', None), None, 'identifier should be created') + self.assertNotEqual( + response.data["catalog_json"].get("identifier", None), + None, + "identifier should be created", + ) def test_research_dataset_schema_missing_ok(self): - self.new_test_data['catalog_json'].pop('research_dataset_schema', None) - response = self.client.post('/rest/datacatalogs', self.new_test_data, format="json") + self.new_test_data["catalog_json"].pop("research_dataset_schema", None) + response = self.client.post("/rest/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_research_dataset_schema_not_found_error(self): - self.new_test_data['catalog_json']['research_dataset_schema'] = 'notfound' - response = self.client.post('/rest/datacatalogs', self.new_test_data, format="json") + self.new_test_data["catalog_json"]["research_dataset_schema"] = "notfound" + response = self.client.post("/rest/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_disallow_versioning_in_harvested_catalogs(self): - self.new_test_data['catalog_json']['dataset_versioning'] = True - self.new_test_data['catalog_json']['harvested'] = True - response = self.client.post('/rest/datacatalogs', self.new_test_data, format="json") + self.new_test_data["catalog_json"]["dataset_versioning"] = True + self.new_test_data["catalog_json"]["harvested"] = True + response = self.client.post("/rest/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('versioning' in response.data['detail'][0], True, response.data) + self.assertEqual("versioning" in response.data["detail"][0], True, response.data) def test_create_identifier_already_exists(self): - response = self.client.post('/rest/datacatalogs', self.new_test_data, format="json") + response = self.client.post("/rest/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.post('/rest/datacatalogs', self.new_test_data, format="json") + response = self.client.post("/rest/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('already exists' in response.data['catalog_json']['identifier'][0], - True, response.data) + self.assertEqual( + "already exists" in response.data["catalog_json"]["identifier"][0], + True, + response.data, + ) def test_delete(self): - response = self.client.delete('/rest/datacatalogs/1') + response = self.client.delete("/rest/datacatalogs/1") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) dc_deleted = DataCatalog.objects_unfiltered.get(pk=1) self.assertEqual(dc_deleted.removed, True) - self.assertEqual(dc_deleted.date_modified, dc_deleted.date_removed, 'date_modified should be updated') + self.assertEqual( + dc_deleted.date_modified, + dc_deleted.date_removed, + "date_modified should be updated", + ) def test_publisher_name_is_required(self): """ @@ -78,12 +88,13 @@ def test_publisher_name_is_required(self): """ catalog = deepcopy(self.new_test_data) - catalog['catalog_json']['publisher'].pop('identifier', None) - catalog['catalog_json']['publisher'].pop('name', None) + catalog["catalog_json"]["publisher"].pop("identifier", None) + catalog["catalog_json"]["publisher"].pop("name", None) - response = self.client.post('/rest/datacatalogs', catalog, format="json") + response = self.client.post("/rest/datacatalogs", catalog, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('name' in response.data['catalog_json'][0], response.data) + self.assertTrue("name" in response.data["catalog_json"][0], response.data) + class DataCatalogApiWriteReferenceDataTests(DataCatalogApiWriteCommon): """ @@ -92,15 +103,15 @@ class DataCatalogApiWriteReferenceDataTests(DataCatalogApiWriteCommon): """ def test_create_data_catalog_with_invalid_reference_data(self): - dc = self.new_test_data['catalog_json'] - dc['field_of_science'][0]['identifier'] = 'nonexisting' - dc['language'][0]['identifier'] = 'nonexisting' - dc['access_rights']['access_type'][0]['identifier'] = 'nonexisting' - dc['access_rights']['license'][0]['identifier'] = 'nonexisting' - response = self.client.post('/rest/datacatalogs', self.new_test_data, format="json") + dc = self.new_test_data["catalog_json"] + dc["field_of_science"][0]["identifier"] = "nonexisting" + dc["language"][0]["identifier"] = "nonexisting" + dc["access_rights"]["access_type"][0]["identifier"] = "nonexisting" + dc["access_rights"]["license"][0]["identifier"] = "nonexisting" + response = self.client.post("/rest/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('catalog_json' in response.data.keys(), True) - self.assertEqual(len(response.data['catalog_json']), 4) + self.assertEqual("catalog_json" in response.data.keys(), True) + self.assertEqual(len(response.data["catalog_json"]), 4) def test_create_data_catalog_populate_fields_from_reference_data(self): """ @@ -111,74 +122,93 @@ def test_create_data_catalog_populate_fields_from_reference_data(self): 3) Check that labels have also been copied to data catalog to their approriate fields """ cache = RedisClient() - refdata = cache.get('reference_data')['reference_data'] - orgdata = cache.get('reference_data')['organization_data'] + refdata = cache.get("reference_data")["reference_data"] + orgdata = cache.get("reference_data")["organization_data"] refs = {} data_types = [ - 'access_type', - 'field_of_science', - 'language', - 'license', + "access_type", + "field_of_science", + "language", + "license", ] # the values in these selected entries will be used throghout the rest of the test case for dtype in data_types: entry = refdata[dtype][0] refs[dtype] = { - 'code': entry['code'], - 'uri': entry['uri'], - 'label': entry.get('label', None), + "code": entry["code"], + "uri": entry["uri"], + "label": entry.get("label", None), } - refs['organization'] = { - 'uri': orgdata['organization'][1]['uri'], - 'code': orgdata['organization'][1]['code'], - 'label': orgdata['organization'][1]['label'], + refs["organization"] = { + "uri": orgdata["organization"][1]["uri"], + "code": orgdata["organization"][1]["code"], + "label": orgdata["organization"][1]["label"], } # replace the relations with objects that have only the identifier set with code as value, # to easily check that label was populated (= that it appeared in the dataset after create) # without knowing its original value from the generated test data - dc = self.new_test_data['catalog_json'] - dc['field_of_science'][0] = {'identifier': refs['field_of_science']['code']} - dc['language'][0] = {'identifier': refs['language']['code']} - dc['access_rights']['access_type'][0] = {'identifier': refs['access_type']['code']} - dc['access_rights']['license'][0] = {'identifier': refs['license']['code']} + dc = self.new_test_data["catalog_json"] + dc["field_of_science"][0] = {"identifier": refs["field_of_science"]["code"]} + dc["language"][0] = {"identifier": refs["language"]["code"]} + dc["access_rights"]["access_type"][0] = {"identifier": refs["access_type"]["code"]} + dc["access_rights"]["license"][0] = {"identifier": refs["license"]["code"]} # these have other required fields, so only update the identifier with code - dc['publisher']['identifier'] = refs['organization']['code'] - dc['access_rights']['has_rights_related_agent'][0]['identifier'] = refs['organization']['code'] + dc["publisher"]["identifier"] = refs["organization"]["code"] + dc["access_rights"]["has_rights_related_agent"][0]["identifier"] = refs["organization"][ + "code" + ] # ensure that name is not required if reference data is used, i.e. identifier is given - dc['publisher'].pop('name', None) + dc["publisher"].pop("name", None) - response = self.client.post('/rest/datacatalogs', self.new_test_data, format="json") + response = self.client.post("/rest/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('catalog_json' in response.data.keys(), True) + self.assertEqual("catalog_json" in response.data.keys(), True) - new_dc = response.data['catalog_json'] + new_dc = response.data["catalog_json"] self._assert_uri_copied_to_identifier(refs, new_dc) self._assert_label_copied_to_pref_label(refs, new_dc) self._assert_label_copied_to_title(refs, new_dc) self._assert_label_copied_to_name(refs, new_dc) def _assert_uri_copied_to_identifier(self, refs, new_dc): - self.assertEqual(refs['field_of_science']['uri'], new_dc['field_of_science'][0]['identifier']) - self.assertEqual(refs['language']['uri'], new_dc['language'][0]['identifier']) - self.assertEqual(refs['access_type']['uri'], new_dc['access_rights']['access_type'][0]['identifier']) - self.assertEqual(refs['license']['uri'], new_dc['access_rights']['license'][0]['identifier']) - self.assertEqual(refs['organization']['uri'], new_dc['publisher']['identifier']) - self.assertEqual(refs['organization']['uri'], - new_dc['access_rights']['has_rights_related_agent'][0]['identifier']) + self.assertEqual( + refs["field_of_science"]["uri"], new_dc["field_of_science"][0]["identifier"] + ) + self.assertEqual(refs["language"]["uri"], new_dc["language"][0]["identifier"]) + self.assertEqual( + refs["access_type"]["uri"], + new_dc["access_rights"]["access_type"][0]["identifier"], + ) + self.assertEqual( + refs["license"]["uri"], new_dc["access_rights"]["license"][0]["identifier"] + ) + self.assertEqual(refs["organization"]["uri"], new_dc["publisher"]["identifier"]) + self.assertEqual( + refs["organization"]["uri"], + new_dc["access_rights"]["has_rights_related_agent"][0]["identifier"], + ) def _assert_label_copied_to_pref_label(self, refs, new_dc): - self.assertEqual(refs['field_of_science']['label'], new_dc['field_of_science'][0].get('pref_label', None)) - self.assertEqual(refs['access_type']['label'], - new_dc['access_rights']['access_type'][0].get('pref_label', None)) + self.assertEqual( + refs["field_of_science"]["label"], + new_dc["field_of_science"][0].get("pref_label", None), + ) + self.assertEqual( + refs["access_type"]["label"], + new_dc["access_rights"]["access_type"][0].get("pref_label", None), + ) def _assert_label_copied_to_title(self, refs, new_dc): - self.assertEqual(refs['license']['label'], new_dc['access_rights']['license'][0].get('title', None)) + self.assertEqual( + refs["license"]["label"], + new_dc["access_rights"]["license"][0].get("title", None), + ) def _assert_label_copied_to_name(self, refs, new_dc): - self.assertEqual(refs['organization']['label'], new_dc['publisher']['name']) + self.assertEqual(refs["organization"]["label"], new_dc["publisher"]["name"]) diff --git a/src/metax_api/tests/api/rest/base/views/datasets/read.py b/src/metax_api/tests/api/rest/base/views/datasets/read.py index 18825c13..98b255bc 100755 --- a/src/metax_api/tests/api/rest/base/views/datasets/read.py +++ b/src/metax_api/tests/api/rest/base/views/datasets/read.py @@ -5,8 +5,8 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from copy import deepcopy import urllib.parse +from copy import deepcopy from datetime import timedelta import responses @@ -27,28 +27,32 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(CatalogRecordApiReadCommon, cls).setUpClass() def setUp(self): - self.cr_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=0) - self.pk = self.cr_from_test_data['id'] - self.metadata_version_identifier = self.cr_from_test_data['research_dataset']['metadata_version_identifier'] - self.preferred_identifier = self.cr_from_test_data['research_dataset']['preferred_identifier'] - self.identifier = self.cr_from_test_data['identifier'] + self.cr_from_test_data = self._get_object_from_test_data("catalogrecord", requested_index=0) + self.pk = self.cr_from_test_data["id"] + self.metadata_version_identifier = self.cr_from_test_data["research_dataset"][ + "metadata_version_identifier" + ] + self.preferred_identifier = self.cr_from_test_data["research_dataset"][ + "preferred_identifier" + ] + self.identifier = self.cr_from_test_data["identifier"] self._use_http_authorization() def create_legacy_dataset(self): cr = deepcopy(self.cr_from_test_data) - cr['data_catalog'] = settings.LEGACY_CATALOGS[0] - cr.pop('identifier') - cr['research_dataset']['preferred_identifier'] = 'ldhkrfdwam' - cr['research_dataset'].pop('files') - cr['research_dataset'].pop('total_files_byte_size') - response = self.client.post('/rest/v2/datasets', cr, format="json") + cr["data_catalog"] = settings.LEGACY_CATALOGS[0] + cr.pop("identifier") + cr["research_dataset"]["preferred_identifier"] = "ldhkrfdwam" + cr["research_dataset"].pop("files") + cr["research_dataset"].pop("total_files_byte_size") + response = self.client.post("/rest/v2/datasets", cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - return response.data['id'] + return response.data["id"] class CatalogRecordApiReadBasicTests(CatalogRecordApiReadCommon): @@ -58,7 +62,7 @@ class CatalogRecordApiReadBasicTests(CatalogRecordApiReadCommon): """ def test_read_catalog_record_list(self): - response = self.client.get('/rest/datasets') + response = self.client.get("/rest/datasets") self.assertEqual(response.status_code, status.HTTP_200_OK) def test_read_catalog_record_list_by_ids(self): @@ -67,99 +71,108 @@ def test_read_catalog_record_list_by_ids(self): cr_id_lists = [ [1, 4, 6], [1, 4, 6, 777], - ['cr955e904-e3dd-4d7e-99f1-3fed446f96d7', - 'cr955e904-e3dd-4d7e-99f1-3fed446f96d6', - 'cr955e904-e3dd-4d7e-99f1-3fed446f96d5'], - ['cr955e904-e3dd-4d7e-99f1-3fed446f96d7', - 'cr955e904-e3dd-4d7e-99f1-3fed446f96d6', - 'cr955e904-e3dd-4d7e-99f1-3fed446f96d5', - 'something'] + [ + "cr955e904-e3dd-4d7e-99f1-3fed446f96d7", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d6", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d5", + ], + [ + "cr955e904-e3dd-4d7e-99f1-3fed446f96d7", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d6", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d5", + "something", + ], ] for id_list in cr_id_lists: - response = self.client.post('/rest/datasets/list', id_list, format="json") + response = self.client.post("/rest/datasets/list", id_list, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 3) + self.assertEqual(len(response.data["results"]), 3) # check that fields parameter works - response = self.client.post('/rest/datasets/list?fields=id', id_list, format="json") + response = self.client.post("/rest/datasets/list?fields=id", id_list, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 3) - self.assertEqual(len(response.data['results'][0].keys()), 1) - self.assertEqual(list(response.data['results'][0].keys()), ['id']) + self.assertEqual(len(response.data["results"]), 3) + self.assertEqual(len(response.data["results"][0].keys()), 1) + self.assertEqual(list(response.data["results"][0].keys()), ["id"]) # Failing/empty tests - cr_bad_lists = [ - ['something'], - [999, 777] - ] + cr_bad_lists = [["something"], [999, 777]] for bad_list in cr_bad_lists: - response = self.client.post('/rest/datasets/list', bad_list, format="json") + response = self.client.post("/rest/datasets/list", bad_list, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['results'], []) + self.assertEqual(response.data["results"], []) - response = self.client.post('/rest/datasets/list', [], format="json") + response = self.client.post("/rest/datasets/list", [], format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('Received empty list of identifiers' in response.data['detail']) + self.assertTrue("Received empty list of identifiers" in response.data["detail"]) def test_read_catalog_record_details_by_pk(self): - response = self.client.get('/rest/datasets/%s' % self.pk) + response = self.client.get("/rest/datasets/%s" % self.pk) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['identifier'], self.identifier) - self.assertEqual('identifier' in response.data['data_catalog'], True) + self.assertEqual(response.data["identifier"], self.identifier) + self.assertEqual("identifier" in response.data["data_catalog"], True) def test_read_catalog_record_details_by_identifier(self): - response = self.client.get('/rest/datasets/%s' % self.identifier) + response = self.client.get("/rest/datasets/%s" % self.identifier) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['identifier'], - self.identifier) + self.assertEqual(response.data["identifier"], self.identifier) def test_get_by_preferred_identifier(self): cr = CatalogRecord.objects.get(pk=1) - cr.research_dataset['preferred_identifier'] = '%s-/uhoh/special.chars?all&around' % cr.preferred_identifier + cr.research_dataset["preferred_identifier"] = ( + "%s-/uhoh/special.chars?all&around" % cr.preferred_identifier + ) cr.force_save() - response = self.client.get('/rest/datasets?preferred_identifier=%s' % - urllib.parse.quote(cr.preferred_identifier)) + response = self.client.get( + "/rest/datasets?preferred_identifier=%s" % urllib.parse.quote(cr.preferred_identifier) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], cr.preferred_identifier) + self.assertEqual( + response.data["research_dataset"]["preferred_identifier"], + cr.preferred_identifier, + ) def test_get_removed_by_preferred_identifier(self): self._use_http_authorization() - response = self.client.delete('/rest/datasets/%s' % self.identifier) + response = self.client.delete("/rest/datasets/%s" % self.identifier) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - response = self.client.get('/rest/datasets?preferred_identifier=%s&removed=true' % - urllib.parse.quote(self.preferred_identifier)) + response = self.client.get( + "/rest/datasets?preferred_identifier=%s&removed=true" + % urllib.parse.quote(self.preferred_identifier) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_get_by_preferred_identifier_search_prefers_oldest_data_catalog(self): - ''' + """ Search by preferred_identifier should prefer hits from oldest created catalogs which are assumed to be att/fairdata catalogs. - ''' + """ # get a cr that has alternate records - cr = self._get_object_from_test_data('catalogrecord', requested_index=9) - pid = cr['research_dataset']['preferred_identifier'] + cr = self._get_object_from_test_data("catalogrecord", requested_index=9) + pid = cr["research_dataset"]["preferred_identifier"] # verify there are more than one record with same pid! count = CatalogRecord.objects.filter(research_dataset__preferred_identifier=pid).count() - self.assertEqual(count > 1, True, 'makes no sense to test with a pid that exists only once') + self.assertEqual(count > 1, True, "makes no sense to test with a pid that exists only once") # the retrieved record should be the one that is in catalog 1 - response = self.client.get('/rest/datasets?preferred_identifier=%s' % - urllib.parse.quote(pid)) - self.assertEqual('alternate_record_set' in response.data, True) - self.assertEqual(response.data['data_catalog']['id'], cr['data_catalog']) + response = self.client.get( + "/rest/datasets?preferred_identifier=%s" % urllib.parse.quote(pid) + ) + self.assertEqual("alternate_record_set" in response.data, True) + self.assertEqual(response.data["data_catalog"]["id"], cr["data_catalog"]) def test_read_catalog_record_details_not_found(self): - response = self.client.get('/rest/datasets/shouldnotexist') + response = self.client.get("/rest/datasets/shouldnotexist") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_read_catalog_record_metadata_version_identifiers(self): - response = self.client.get('/rest/datasets/metadata_version_identifiers') + response = self.client.get("/rest/datasets/metadata_version_identifiers") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(isinstance(response.data, list)) self.assertTrue(len(response.data) > 0) @@ -168,7 +181,7 @@ def test_get_unique_preferred_identifiers(self): """ Get all unique preferred_identifiers, no matter if they are the latest dataset version or not. """ - response = self.client.get('/rest/datasets/unique_preferred_identifiers') + response = self.client.get("/rest/datasets/unique_preferred_identifiers") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(isinstance(response.data, list)) self.assertTrue(len(response.data) > 0) @@ -178,14 +191,14 @@ def test_get_unique_preferred_identifiers(self): self._create_new_ds() self._create_new_ds() - response = self.client.get('/rest/datasets/unique_preferred_identifiers') - self.assertEqual(len(response.data) - ids_len, 2, 'should be two new PIDs') + response = self.client.get("/rest/datasets/unique_preferred_identifiers") + self.assertEqual(len(response.data) - ids_len, 2, "should be two new PIDs") def test_get_latest_unique_preferred_identifiers(self): """ Get all unique preferred_identifiers, but only from the latest dataset versions. """ - response = self.client.get('/rest/datasets/unique_preferred_identifiers?latest') + response = self.client.get("/rest/datasets/unique_preferred_identifiers?latest") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(isinstance(response.data, list)) self.assertTrue(len(response.data) > 0) @@ -195,70 +208,84 @@ def test_get_latest_unique_preferred_identifiers(self): # files change cr = CatalogRecord.objects.get(pk=1) - new_file_id = cr.files.all().order_by('-id').first().id + 1 - file_from_testdata = self._get_object_from_test_data('file', requested_index=new_file_id) + new_file_id = cr.files.all().order_by("-id").first().id + 1 + file_from_testdata = self._get_object_from_test_data("file", requested_index=new_file_id) # warning, this is actual file metadata, would not pass schema validation if sent through api - cr.research_dataset['files'] = [file_from_testdata] + cr.research_dataset["files"] = [file_from_testdata] cr.save() - response = self.client.get('/rest/datasets/unique_preferred_identifiers?latest') - self.assertEqual(ids_len, len(response.data), 'count should stay the same') + response = self.client.get("/rest/datasets/unique_preferred_identifiers?latest") + self.assertEqual(ids_len, len(response.data), "count should stay the same") # create new self._create_new_ds() self._create_new_ds() - response = self.client.get('/rest/datasets/unique_preferred_identifiers?latest') - self.assertEqual(len(response.data) - ids_len, 2, 'should be two new PIDs') + response = self.client.get("/rest/datasets/unique_preferred_identifiers?latest") + self.assertEqual(len(response.data) - ids_len, 2, "should be two new PIDs") def test_expand_relations(self): cr = CatalogRecord.objects.get(pk=1) cr.contract_id = 1 cr.force_save() - response = self.client.get('/rest/datasets/1?expand_relation=data_catalog,contract') + response = self.client.get("/rest/datasets/1?expand_relation=data_catalog,contract") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('catalog_json' in response.data['data_catalog'], True, response.data['data_catalog']) - self.assertEqual('contract_json' in response.data['contract'], True, response.data['contract']) + self.assertEqual( + "catalog_json" in response.data["data_catalog"], + True, + response.data["data_catalog"], + ) + self.assertEqual( + "contract_json" in response.data["contract"], + True, + response.data["contract"], + ) def test_strip_sensitive_fields(self): """ Strip fields not intended for general public """ + def _check_fields(obj): - for sensitive_field in ['email', 'telephone', 'phone']: - self.assertEqual(sensitive_field not in obj['research_dataset']['curator'][0], True, - 'field %s should have been stripped' % sensitive_field) + for sensitive_field in ["email", "telephone", "phone"]: + self.assertEqual( + sensitive_field not in obj["research_dataset"]["curator"][0], + True, + "field %s should have been stripped" % sensitive_field, + ) for cr in CatalogRecord.objects.filter(pk__in=(1, 2, 3)): - cr.research_dataset['curator'][0].update({ - 'email': 'email@mail.com', - 'phone': '123124', - 'telephone': '123124', - }) + cr.research_dataset["curator"][0].update( + { + "email": "email@mail.com", + "phone": "123124", + "telephone": "123124", + } + ) cr.force_save() self.client._credentials = {} - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) _check_fields(response.data) - response = self.client.get('/rest/datasets') + response = self.client.get("/rest/datasets") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - for obj in response.data['results']: + for obj in response.data["results"]: _check_fields(obj) - response = self.client.get('/rest/datasets?no_pagination') + response = self.client.get("/rest/datasets?no_pagination") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) for obj in response.data: _check_fields(obj) def _create_new_ds(self): - new_cr = self.client.get('/rest/datasets/2').data - new_cr.pop('id') - new_cr['research_dataset'].pop('preferred_identifier') - new_cr.pop('identifier') + new_cr = self.client.get("/rest/datasets/2").data + new_cr.pop("id") + new_cr["research_dataset"].pop("preferred_identifier") + new_cr.pop("identifier") self._use_http_authorization() - response = self.client.post('/rest/datasets', new_cr, format='json') + response = self.client.post("/rest/datasets", new_cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) @@ -269,119 +296,145 @@ class CatalogRecordApiReadBasicAuthorizationTests(CatalogRecordApiReadCommon): # THE OK TESTS - def test_returns_all_file_dir_info_for_open_catalog_record_if_no_authorization(self): + def test_returns_all_file_dir_info_for_open_catalog_record_if_no_authorization( + self, + ): open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() # Verify all file and dir details info is returned for open cr /rest/datasets/ without # authorization - self._assert_ok(open_cr_json, 'no') + self._assert_ok(open_cr_json, "no") - def test_returns_all_file_dir_info_for_login_catalog_record_if_no_authorization(self): - login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(use_login_access_type=True) + def test_returns_all_file_dir_info_for_login_catalog_record_if_no_authorization( + self, + ): + login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details( + use_login_access_type=True + ) # Verify all file and dir details info is returned for login cr /rest/datasets/ without authorization - self._assert_ok(login_cr_json, 'no') + self._assert_ok(login_cr_json, "no") @responses.activate - def test_returns_all_file_dir_info_for_open_catalog_record_if_owner_authorization(self): + def test_returns_all_file_dir_info_for_open_catalog_record_if_owner_authorization( + self, + ): self.create_end_user_data_catalogs() open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(True) # Verify all file and dir details info is returned for open owner-owned cr /rest/datasets/ with # owner authorization - self._assert_ok(open_cr_json, 'owner') + self._assert_ok(open_cr_json, "owner") @responses.activate - def test_returns_all_file_dir_info_for_login_catalog_record_if_owner_authorization(self): + def test_returns_all_file_dir_info_for_login_catalog_record_if_owner_authorization( + self, + ): self.create_end_user_data_catalogs() - login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(set_owner=True, - use_login_access_type=True) + login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details( + set_owner=True, use_login_access_type=True + ) # Verify all file and dir details info is returned for login owner-owned cr /rest/datasets/ with # owner authorization - self._assert_ok(login_cr_json, 'owner') + self._assert_ok(login_cr_json, "owner") - def test_returns_all_file_dir_info_for_restricted_catalog_record_if_service_authorization(self): + def test_returns_all_file_dir_info_for_restricted_catalog_record_if_service_authorization( + self, + ): restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() # Verify all file and dir details info is returned for restricted cr /rest/datasets/ with # service authorization - self._assert_ok(restricted_cr_json, 'service') + self._assert_ok(restricted_cr_json, "service") @responses.activate - def test_returns_all_file_dir_info_for_restricted_catalog_record_if_owner_authorization(self): + def test_returns_all_file_dir_info_for_restricted_catalog_record_if_owner_authorization( + self, + ): self.create_end_user_data_catalogs() - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details(True) + restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details( + True + ) # Verify all file and dir details info is returned for restricted owner-owned cr /rest/datasets/ with # owner authorization - self._assert_ok(restricted_cr_json, 'owner') + self._assert_ok(restricted_cr_json, "owner") - def test_returns_all_file_dir_info_for_embargoed_catalog_record_if_available_reached_and_no_authorization(self): - available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) + def test_returns_all_file_dir_info_for_embargoed_catalog_record_if_available_reached_and_no_authorization( + self, + ): + available_embargoed_cr_json = ( + self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) + ) # Verify all file and dir details info is returned for embargoed cr /rest/datasets/ when # embargo date has been reached without authorization - self._assert_ok(available_embargoed_cr_json, 'no') + self._assert_ok(available_embargoed_cr_json, "no") # THE FORBIDDEN TESTS - def test_returns_limited_file_dir_info_for_restricted_catalog_record_if_no_authorization(self): + def test_returns_limited_file_dir_info_for_restricted_catalog_record_if_no_authorization( + self, + ): restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() # Verify limited file and dir info for restricted cr /rest/datasets/ without authorization - self._assert_limited_or_no_file_dir_info(restricted_cr_json, 'no') + self._assert_limited_or_no_file_dir_info(restricted_cr_json, "no") - def test_no_file_dir_info_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization(self): - not_available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details( - False) + def test_no_file_dir_info_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization( + self, + ): + not_available_embargoed_cr_json = ( + self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(False) + ) # Verify no file and dir info for embargoed cr /rest/datasets/ when embargo date has not # been reached without authorization - self._assert_limited_or_no_file_dir_info(not_available_embargoed_cr_json, 'no') + self._assert_limited_or_no_file_dir_info(not_available_embargoed_cr_json, "no") def _assert_limited_or_no_file_dir_info(self, cr_json, credentials_type): self._set_http_authorization(credentials_type) - file_amt = len(cr_json['research_dataset']['files']) - dir_amt = len(cr_json['research_dataset']['directories']) - pk = cr_json['id'] + file_amt = len(cr_json["research_dataset"]["files"]) + dir_amt = len(cr_json["research_dataset"]["directories"]) + pk = cr_json["id"] - response = self.client.get('/rest/datasets/{0}?file_details'.format(pk)) + response = self.client.get("/rest/datasets/{0}?file_details".format(pk)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['files']), file_amt) - self.assertEqual(len(response.data['research_dataset']['directories']), dir_amt) + self.assertEqual(len(response.data["research_dataset"]["files"]), file_amt) + self.assertEqual(len(response.data["research_dataset"]["directories"]), dir_amt) - for f in response.data['research_dataset']['files']: - self.assertTrue('details' in f) + for f in response.data["research_dataset"]["files"]: + self.assertTrue("details" in f) # The below assert is a bit arbitrary - self.assertFalse('identifier' in f) - for d in response.data['research_dataset']['directories']: - self.assertTrue('details' in d) + self.assertFalse("identifier" in f) + for d in response.data["research_dataset"]["directories"]: + self.assertTrue("details" in d) # The below assert is a bit arbitrary - self.assertFalse('identifier' in d) + self.assertFalse("identifier" in d) def _assert_ok(self, cr_json, credentials_type): self._set_http_authorization(credentials_type) - file_amt = len(cr_json['research_dataset']['files']) - dir_amt = len(cr_json['research_dataset']['directories']) - pk = cr_json['id'] + file_amt = len(cr_json["research_dataset"]["files"]) + dir_amt = len(cr_json["research_dataset"]["directories"]) + pk = cr_json["id"] - response = self.client.get('/rest/datasets/{0}?file_details'.format(pk)) + response = self.client.get("/rest/datasets/{0}?file_details".format(pk)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['files']), file_amt) - self.assertEqual(len(response.data['research_dataset']['directories']), dir_amt) + self.assertEqual(len(response.data["research_dataset"]["files"]), file_amt) + self.assertEqual(len(response.data["research_dataset"]["directories"]), dir_amt) - for f in response.data['research_dataset']['files']: - self.assertTrue('details' in f) + for f in response.data["research_dataset"]["files"]: + self.assertTrue("details" in f) # The below assert is a bit arbitrary - self.assertTrue('identifier' in f) - for d in response.data['research_dataset']['directories']: - self.assertTrue('details' in d) + self.assertTrue("identifier" in f) + for d in response.data["research_dataset"]["directories"]: + self.assertTrue("details" in d) # The below assert is a bit arbitrary - self.assertTrue('identifier' in d) + self.assertTrue("identifier" in d) class CatalogRecordApiReadPreservationStateTests(CatalogRecordApiReadCommon): @@ -390,134 +443,187 @@ class CatalogRecordApiReadPreservationStateTests(CatalogRecordApiReadCommon): """ def test_read_catalog_record_search_by_preservation_state(self): - ''' + """ Various simple filtering requests - ''' - for queryparam in ('preservation_state', 'state'): - response = self.client.get('/rest/datasets?{}=0'.format(queryparam)) + """ + for queryparam in ("preservation_state", "state"): + response = self.client.get("/rest/datasets?{}=0".format(queryparam)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']) > 2, True, - 'There should have been multiple results for state=0 request') + self.assertEqual( + len(response.data["results"]) > 2, + True, + "There should have been multiple results for state=0 request", + ) - response = self.client.get('/rest/datasets?{}=10'.format(queryparam)) + response = self.client.get("/rest/datasets?{}=10".format(queryparam)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 2) + self.assertEqual(len(response.data["results"]), 2) - response = self.client.get('/rest/datasets?{}=40'.format(queryparam)) + response = self.client.get("/rest/datasets?{}=40".format(queryparam)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) + self.assertEqual(len(response.data["results"]), 1) def test_read_catalog_record_search_by_preservation_state_666(self): - for queryparam in ('preservation_state', 'state'): - response = self.client.get('/rest/datasets?{}=666'.format(queryparam)) + for queryparam in ("preservation_state", "state"): + response = self.client.get("/rest/datasets?{}=666".format(queryparam)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 0, 'should return empty list') + self.assertEqual(len(response.data["results"]), 0, "should return empty list") def test_read_catalog_record_search_by_preservation_state_many(self): - for queryparam in ('preservation_state', 'state'): - response = self.client.get('/rest/datasets?{}=10,40'.format(queryparam)) + for queryparam in ("preservation_state", "state"): + response = self.client.get("/rest/datasets?{}=10,40".format(queryparam)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 3) - self.assertEqual(response.data['results'][0]['preservation_state'], 10) - self.assertEqual(response.data['results'][1]['preservation_state'], 10) - self.assertEqual(response.data['results'][2]['preservation_state'], 40) + self.assertEqual(len(response.data["results"]), 3) + self.assertEqual(response.data["results"][0]["preservation_state"], 10) + self.assertEqual(response.data["results"][1]["preservation_state"], 10) + self.assertEqual(response.data["results"][2]["preservation_state"], 40) def test_read_catalog_record_search_by_preservation_state_invalid_value(self): - response = self.client.get('/rest/datasets?state=1,a') + response = self.client.get("/rest/datasets?state=1,a") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('is not an integer' in response.data['state'][0], True, - 'Error should say letter a is not an integer') + self.assertEqual( + "is not an integer" in response.data["state"][0], + True, + "Error should say letter a is not an integer", + ) - response = self.client.get('/rest/datasets?preservation_state=1,a') + response = self.client.get("/rest/datasets?preservation_state=1,a") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('is not an integer' in response.data['preservation_state'][0], True, - 'Error should say letter a is not an integer') + self.assertEqual( + "is not an integer" in response.data["preservation_state"][0], + True, + "Error should say letter a is not an integer", + ) -class CatalogRecordApiReadActorFilter(CatalogRecordApiReadCommon): +class CatalogRecordApiReadActorFilter(CatalogRecordApiReadCommon): def test_agents_and_actors(self): # set test conditions cr = CatalogRecord.objects.get(pk=11) - cr.research_dataset['curator'] = [] - cr.research_dataset['curator'].append({ - '@type': 'Person', - 'name': 'Tarmo Termiitti', - 'member_of': { - 'identifier': 'org_identifier', - 'name': { - 'en': 'Unique Organization' - } + cr.research_dataset["curator"] = [] + cr.research_dataset["curator"].append( + { + "@type": "Person", + "name": "Tarmo Termiitti", + "member_of": { + "identifier": "org_identifier", + "name": {"en": "Unique Organization"}, + }, } - }) - cr.research_dataset['curator'].append({ '@type': 'Person', 'name': 'Keijo Kottarainen' }) - cr.research_dataset['curator'].append({ '@type': 'Person', 'name': 'Janus Järvinen' }) - cr.research_dataset['curator'].append({ '@type': 'Person', 'name': 'Laina Sakkonen' }) - cr.research_dataset['curator'].append({ - '@type': 'Person', - 'name': 'Kaisa Kuraattori', - 'member_of': { - 'identifier': 'org_identifier', - 'name': { - 'en': 'Happy Organization' - } + ) + cr.research_dataset["curator"].append({"@type": "Person", "name": "Keijo Kottarainen"}) + cr.research_dataset["curator"].append({"@type": "Person", "name": "Janus Järvinen"}) + cr.research_dataset["curator"].append({"@type": "Person", "name": "Laina Sakkonen"}) + cr.research_dataset["curator"].append( + { + "@type": "Person", + "name": "Kaisa Kuraattori", + "member_of": { + "identifier": "org_identifier", + "name": {"en": "Happy Organization"}, + }, } - }) - cr.research_dataset['creator'] = [] - cr.research_dataset['creator'].append({ '@type': 'Organization', 'name': { 'en': 'Unique Organization'} }) - cr.research_dataset['creator'].append({ '@type': 'Organization', 'name': { 'en': 'Happy Organization'} }) - cr.research_dataset['creator'].append({ '@type': 'Organization', 'name': { 'en': 'Sad Organization'} }) - cr.research_dataset['creator'].append({ '@type': 'Organization', 'name': { 'en': 'Brilliant Organization'} }) - cr.research_dataset['creator'].append({ '@type': 'Organization', 'name': {'en': 'Wonderful Organization'} }) - cr.research_dataset['publisher']['name'] = {} - cr.research_dataset['publisher']['name'] = { 'fi': 'Originaali Organisaatio' } + ) + cr.research_dataset["creator"] = [] + cr.research_dataset["creator"].append( + {"@type": "Organization", "name": {"en": "Unique Organization"}} + ) + cr.research_dataset["creator"].append( + {"@type": "Organization", "name": {"en": "Happy Organization"}} + ) + cr.research_dataset["creator"].append( + {"@type": "Organization", "name": {"en": "Sad Organization"}} + ) + cr.research_dataset["creator"].append( + {"@type": "Organization", "name": {"en": "Brilliant Organization"}} + ) + cr.research_dataset["creator"].append( + {"@type": "Organization", "name": {"en": "Wonderful Organization"}} + ) + cr.research_dataset["publisher"]["name"] = {} + cr.research_dataset["publisher"]["name"] = {"fi": "Originaali Organisaatio"} cr.force_save() - response = self.client.get('/rest/datasets?creator_organization=happy') - self.assertEqual(len(response.data['results']), 1, response.data) + response = self.client.get("/rest/datasets?creator_organization=happy") + self.assertEqual(len(response.data["results"]), 1, response.data) - response = self.client.get('/rest/datasets?creator_organization=Brilliant Organization') - self.assertEqual(len(response.data['results']), 1, response.data) + response = self.client.get("/rest/datasets?creator_organization=Brilliant Organization") + self.assertEqual(len(response.data["results"]), 1, response.data) - response = self.client.get('/rest/datasets?curator_person=termiitti') - self.assertEqual(len(response.data['results']), 1, response.data) + response = self.client.get("/rest/datasets?curator_person=termiitti") + self.assertEqual(len(response.data["results"]), 1, response.data) - response = self.client.get('/rest/datasets?curator_person=Laina Sakkonen') - self.assertEqual(len(response.data['results']), 1, response.data) + response = self.client.get("/rest/datasets?curator_person=Laina Sakkonen") + self.assertEqual(len(response.data["results"]), 1, response.data) - response = self.client.get('/rest/datasets?curator_organization=uniqu') - self.assertEqual(len(response.data['results']), 1, response.data) + response = self.client.get("/rest/datasets?curator_organization=uniqu") + self.assertEqual(len(response.data["results"]), 1, response.data) - response = self.client.get('/rest/datasets?curator_organization=Happy Organization') - self.assertEqual(len(response.data['results']), 1, response.data) + response = self.client.get("/rest/datasets?curator_organization=Happy Organization") + self.assertEqual(len(response.data["results"]), 1, response.data) - response = self.client.get('/rest/datasets?publisher_organization=originaali Organisaatio') - self.assertEqual(len(response.data['results']), 1, response.data) + response = self.client.get("/rest/datasets?publisher_organization=originaali Organisaatio") + self.assertEqual(len(response.data["results"]), 1, response.data) - query = 'curator_person=notfound&creator_organization=sad organ&condition_separator=AND' - response = self.client.get('/rest/datasets?%s' % query) - self.assertEqual(len(response.data['results']), 0, response.data) + query = "curator_person=notfound&creator_organization=sad organ&condition_separator=AND" + response = self.client.get("/rest/datasets?%s" % query) + self.assertEqual(len(response.data["results"]), 0, response.data) - query = 'curator_person=notfound&creator_organization=sad organ&condition_separator=OR' - response = self.client.get('/rest/datasets?%s' % query) - self.assertEqual(len(response.data['results']), 1, response.data) + query = "curator_person=notfound&creator_organization=sad organ&condition_separator=OR" + response = self.client.get("/rest/datasets?%s" % query) + self.assertEqual(len(response.data["results"]), 1, response.data) # test filter with pas filter """ Both organization and pas filters use internally Q-filters which are supposed to be AND'ed together. """ metax_user = settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) + self._use_http_authorization( + username=metax_user["username"], password=metax_user["password"] + ) + + response = self.client.get( + "/rest/datasets?pas_filter=janus&creator_organization=sad organization" + ) + self.assertEqual(len(response.data["results"]), 1) + + for queryparam in ("preservation_state", "state"): + response = self.client.get( + "/rest/datasets?{}=10&pas_filter=kaisa&" + "creator_organization=notfound".format(queryparam) + ) + self.assertEqual(len(response.data["results"]), 0) + + def test_agents_and_actors_with_ids(self): + # set test conditions + cr = CatalogRecord.objects.get(pk=11) + cr.research_dataset["curator"] = [] + cr.research_dataset["curator"].append( + { + "@type": "Person", + "name": "Tarmo Termiitti", + "member_of": { + "identifier": "org_identifier", + "name": {"en": "Unique Organization"}, + }, + "identifier": "http://orcid.org/1234-1234-1234-1234", + } + ) + cr.research_dataset["creator"] = [] + cr.research_dataset["creator"].append( + {"@type": "Organization", "name": {"en": "Unique Organization"}, "identifier": "http://uri.suomi.fi/codelist/fairdata/organization/code/1234567"} + ) + cr.force_save() - response = self.client.get('/rest/datasets?pas_filter=janus&creator_organization=sad organization') - self.assertEqual(len(response.data['results']), 1) + # test that querys can be made also with organizational and persons' IDs + response = self.client.get("/rest/datasets?creator_organization=1234567") + self.assertEqual(len(response.data["results"]), 1, response.data) - for queryparam in ('preservation_state', 'state'): - response = self.client.get('/rest/datasets?{}=10&pas_filter=kaisa&' - 'creator_organization=notfound'.format(queryparam)) - self.assertEqual(len(response.data['results']), 0) + response = self.client.get("/rest/datasets?curator_person=1234-1234-1234-1234") + self.assertEqual(len(response.data["results"]), 1, response.data) -class CatalogRecordApiReadPASFilter(CatalogRecordApiReadCommon): +class CatalogRecordApiReadPASFilter(CatalogRecordApiReadCommon): def test_pas_filter(self): """ Test query param pas_filter which should search from various fields using the same search term. @@ -527,54 +633,64 @@ def test_pas_filter(self): cr = CatalogRecord.objects.get(pk=1) cr.preservation_state = 10 cr.contract_id = 1 - cr.research_dataset['title']['en'] = 'Catch me if you can' - cr.research_dataset['title']['fi'] = 'Ota kiinni jos saat' - cr.research_dataset['curator'] = [] - cr.research_dataset['curator'].append({ 'name': 'Seppo Hovi' }) - cr.research_dataset['curator'].append({ 'name': 'Esa Nieminen' }) - cr.research_dataset['curator'].append({ 'name': 'Aku Ankka' }) - cr.research_dataset['curator'].append({ 'name': 'Jaska Jokunen' }) + cr.research_dataset["title"]["en"] = "Catch me if you can" + cr.research_dataset["title"]["fi"] = "Ota kiinni jos saat" + cr.research_dataset["curator"] = [] + cr.research_dataset["curator"].append({"name": "Seppo Hovi"}) + cr.research_dataset["curator"].append({"name": "Esa Nieminen"}) + cr.research_dataset["curator"].append({"name": "Aku Ankka"}) + cr.research_dataset["curator"].append({"name": "Jaska Jokunen"}) cr.force_save() contract = Contract.objects.get(pk=1) - contract.contract_json['title'] = 'An Important Agreement' + contract.contract_json["title"] = "An Important Agreement" contract.save() metax_user = settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) + self._use_http_authorization( + username=metax_user["username"], password=metax_user["password"] + ) # beging testing - for queryparam in ('preservation_state', 'state'): - response = self.client.get('/rest/datasets?{}=10&pas_filter=if you'.format(queryparam)) + for queryparam in ("preservation_state", "state"): + response = self.client.get("/rest/datasets?{}=10&pas_filter=if you".format(queryparam)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) + self.assertEqual(len(response.data["results"]), 1) - response = self.client.get('/rest/datasets?{}=10&pas_filter=kiinni jos'.format(queryparam)) - self.assertEqual(len(response.data['results']), 1) + response = self.client.get( + "/rest/datasets?{}=10&pas_filter=kiinni jos".format(queryparam) + ) + self.assertEqual(len(response.data["results"]), 1) - response = self.client.get('/rest/datasets?{}=10&pas_filter=niemine'.format(queryparam)) - self.assertEqual(len(response.data['results']), 1) + response = self.client.get("/rest/datasets?{}=10&pas_filter=niemine".format(queryparam)) + self.assertEqual(len(response.data["results"]), 1) # more than 3 curators, requires typing exact case-sensitive name... see comments in related code - response = self.client.get('/rest/datasets?{}=10&pas_filter=jokunen'.format(queryparam)) - self.assertEqual(len(response.data['results']), 0) - response = self.client.get('/rest/datasets?{}=10&pas_filter=Jaska Jokunen'.format(queryparam)) - self.assertEqual(len(response.data['results']), 1) + response = self.client.get("/rest/datasets?{}=10&pas_filter=jokunen".format(queryparam)) + self.assertEqual(len(response.data["results"]), 0) + response = self.client.get( + "/rest/datasets?{}=10&pas_filter=Jaska Jokunen".format(queryparam) + ) + self.assertEqual(len(response.data["results"]), 1) # contract_id 1 has several other associated test datasets - response = self.client.get('/rest/datasets?{}=10&pas_filter=agreement'.format(queryparam)) - self.assertEqual(len(response.data['results']), 3) + response = self.client.get( + "/rest/datasets?{}=10&pas_filter=agreement".format(queryparam) + ) + self.assertEqual(len(response.data["results"]), 3) - response = self.client.get('/rest/datasets?{}=10&pas_filter=does not exist'.format(queryparam)) - self.assertEqual(len(response.data['results']), 0) + response = self.client.get( + "/rest/datasets?{}=10&pas_filter=does not exist".format(queryparam) + ) + self.assertEqual(len(response.data["results"]), 0) def test_pas_filter_is_restricted(self): """ Query param is permitted to users metax and tpas. """ - for queryparam in ('preservation_state', 'state'): - response = self.client.get('/rest/datasets?{}=10&pas_filter=hmmm'.format(queryparam)) + for queryparam in ("preservation_state", "state"): + response = self.client.get("/rest/datasets?{}=10&pas_filter=hmmm".format(queryparam)) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -585,151 +701,157 @@ class CatalogRecordApiReadQueryParamsTests(CatalogRecordApiReadCommon): """ def test_read_catalog_record_search_by_curator_1(self): - response = self.client.get('/rest/datasets?curator=id:of:curator:rahikainen') + response = self.client.get("/rest/datasets?curator=id:of:curator:rahikainen") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 10) - self.assertEqual(response.data['results'][0]['research_dataset']['curator'][0]['name'], 'Rahikainen', - 'Curator name is not matching') - self.assertEqual(response.data['results'][4]['research_dataset']['curator'][0]['name'], 'Rahikainen', - 'Curator name is not matching') + self.assertEqual(len(response.data["results"]), 10) + self.assertEqual( + response.data["results"][0]["research_dataset"]["curator"][0]["name"], + "Rahikainen", + "Curator name is not matching", + ) + self.assertEqual( + response.data["results"][4]["research_dataset"]["curator"][0]["name"], + "Rahikainen", + "Curator name is not matching", + ) def test_read_catalog_record_search_by_curator_2(self): - response = self.client.get('/rest/datasets?curator=id:of:curator:jarski') + response = self.client.get("/rest/datasets?curator=id:of:curator:jarski") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 4) - self.assertEqual(response.data['results'][0]['research_dataset']['curator'][0]['name'], 'Jarski', - 'Curator name is not matching') - self.assertEqual(response.data['results'][3]['research_dataset']['curator'][0]['name'], 'Jarski', - 'Curator name is not matching') + self.assertEqual( + response.data["results"][0]["research_dataset"]["curator"][0]["name"], + "Jarski", + "Curator name is not matching", + ) + self.assertEqual( + response.data["results"][3]["research_dataset"]["curator"][0]["name"], + "Jarski", + "Curator name is not matching", + ) def test_read_catalog_record_search_by_curator_not_found_1(self): - response = self.client.get('/rest/datasets?curator=Not Found') + response = self.client.get("/rest/datasets?curator=Not Found") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 0) + self.assertEqual(len(response.data["results"]), 0) def test_read_catalog_record_search_by_curator_not_found_case_sensitivity(self): - response = self.client.get('/rest/datasets?curator=id:of:curator:Rahikainen') + response = self.client.get("/rest/datasets?curator=id:of:curator:Rahikainen") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 0) + self.assertEqual(len(response.data["results"]), 0) def test_read_catalog_record_search_by_curator_and_state_1(self): - for queryparam in ('preservation_state', 'state'): - response = self.client.get('/rest/datasets?curator=id:of:curator:rahikainen&{}=10'.format(queryparam)) + for queryparam in ("preservation_state", "state"): + response = self.client.get( + "/rest/datasets?curator=id:of:curator:rahikainen&{}=10".format(queryparam) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 2) - self.assertEqual(response.data['results'][0]['id'], 2) - self.assertEqual(response.data['results'][0]['preservation_state'], 10) - self.assertEqual(response.data['results'][0]['research_dataset']['curator'][0]['name'], 'Rahikainen', - 'Curator name is not matching') + self.assertEqual(len(response.data["results"]), 2) + self.assertEqual(response.data["results"][0]["id"], 2) + self.assertEqual(response.data["results"][0]["preservation_state"], 10) + self.assertEqual( + response.data["results"][0]["research_dataset"]["curator"][0]["name"], + "Rahikainen", + "Curator name is not matching", + ) def test_read_catalog_record_search_by_curator_and_state_2(self): - for queryparam in ('preservation_state', 'state'): - response = self.client.get('/rest/datasets?curator=id:of:curator:rahikainen&{}=40'.format(queryparam)) + for queryparam in ("preservation_state", "state"): + response = self.client.get( + "/rest/datasets?curator=id:of:curator:rahikainen&{}=40".format(queryparam) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) - self.assertEqual(response.data['results'][0]['id'], 4) - self.assertEqual(response.data['results'][0]['preservation_state'], 40) - self.assertEqual(response.data['results'][0]['research_dataset']['curator'][0]['name'], 'Rahikainen', - 'Curator name is not matching') + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["id"], 4) + self.assertEqual(response.data["results"][0]["preservation_state"], 40) + self.assertEqual( + response.data["results"][0]["research_dataset"]["curator"][0]["name"], + "Rahikainen", + "Curator name is not matching", + ) def test_read_catalog_record_search_by_curator_and_state_not_found(self): - for queryparam in ('preservation_state', 'state'): - response = self.client.get('/rest/datasets?curator=id:of:curator:rahikainen&{}=55'.format(queryparam)) + for queryparam in ("preservation_state", "state"): + response = self.client.get( + "/rest/datasets?curator=id:of:curator:rahikainen&{}=55".format(queryparam) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 0) - - def test_read_catalog_record_search_by_owner_id(self): - cr = CatalogRecord.objects.get(pk=1) - cr.editor = { 'owner_id': '123' } - cr.save() - response = self.client.get('/rest/datasets?owner_id=123') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) - self.assertEqual(response.data['results'][0]['editor']['owner_id'], '123') + self.assertEqual(len(response.data["results"]), 0) def test_read_catalog_record_search_by_creator_id(self): cr = CatalogRecord.objects.get(pk=1) - cr.user_created = '123' + cr.user_created = "123" cr.force_save() - response = self.client.get('/rest/datasets?user_created=123') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) - self.assertEqual(response.data['results'][0]['user_created'], '123') - - def test_read_catalog_record_search_by_editor(self): - response = self.client.get('/rest/datasets?editor=mspaint') + response = self.client.get("/rest/datasets?user_created=123") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 0) - - response = self.client.get('/rest/datasets?editor=qvain') - self.assertEqual(response.status_code, status.HTTP_200_OK) - qvain_records_count = response.data['count'] - self.assertEqual(qvain_records_count > 0, True) - - response = self.client.get('/rest/datasets') - self.assertNotEqual(response.data['count'], qvain_records_count, 'looks like filtering had no effect') + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["user_created"], "123") def test_read_catalog_record_search_by_metadata_provider_user(self): - response = self.client.get('/rest/datasets?metadata_provider_user=123') + response = self.client.get("/rest/datasets?metadata_provider_user=123") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 0) + self.assertEqual(response.data["count"], 0) cr = CatalogRecord.objects.get(pk=1) - cr.metadata_provider_user = '123' + cr.metadata_provider_user = "123" cr.force_save() - response = self.client.get('/rest/datasets?metadata_provider_user=123') + response = self.client.get("/rest/datasets?metadata_provider_user=123") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 1) + self.assertEqual(response.data["count"], 1) def test_read_catalog_record_search_by_metadata_owner_org(self): - owner_org = 'org_id' + owner_org = "org_id" for cr in CatalogRecord.objects.filter(pk__in=[1, 2, 3]): cr.metadata_owner_org = owner_org cr.force_save() - owner_org_2 = 'org_id_2' + owner_org_2 = "org_id_2" for cr in CatalogRecord.objects.filter(pk__in=[4, 5, 6]): cr.metadata_owner_org = owner_org_2 cr.force_save() - response = self.client.get('/rest/datasets?metadata_owner_org=%s' % owner_org) + response = self.client.get("/rest/datasets?metadata_owner_org=%s" % owner_org) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 3) + self.assertEqual(len(response.data["results"]), 3) - response = self.client.get('/rest/datasets?metadata_owner_org=%s,%s' % (owner_org, owner_org_2)) + response = self.client.get( + "/rest/datasets?metadata_owner_org=%s,%s" % (owner_org, owner_org_2) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 6) + self.assertEqual(len(response.data["results"]), 6) def test_filter_by_contract_org_identifier(self): """ Test filtering by contract_org_identifier, which matches using iregex """ metax_user = settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) + self._use_http_authorization( + username=metax_user["username"], password=metax_user["password"] + ) - response = self.client.get('/rest/datasets?contract_org_identifier=2345') + response = self.client.get("/rest/datasets?contract_org_identifier=2345") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 10) + self.assertEqual(len(response.data["results"]), 10) - response = self.client.get('/rest/datasets?contract_org_identifier=1234567-1') - self.assertEqual(len(response.data['results']), 10) + response = self.client.get("/rest/datasets?contract_org_identifier=1234567-1") + self.assertEqual(len(response.data["results"]), 10) - response = self.client.get('/rest/datasets?contract_org_identifier=1234567-123') - self.assertEqual(len(response.data['results']), 0) + response = self.client.get("/rest/datasets?contract_org_identifier=1234567-123") + self.assertEqual(len(response.data["results"]), 0) def test_filter_by_contract_org_identifier_is_restricted(self): - response = self.client.get('/rest/datasets?contract_org_identifier=1234') + response = self.client.get("/rest/datasets?contract_org_identifier=1234") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_read_catalog_record_search_by_data_catalog_id(self): from metax_api.models.data_catalog import DataCatalog # Create a new data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) - dc_id = 'original_dc_identifier' - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/datacatalogs', dc, format="json") + dc = self._get_object_from_test_data("datacatalog", requested_index=0) + dc_id = "original_dc_identifier" + dc["catalog_json"]["identifier"] = dc_id + self.client.post("/rest/datacatalogs", dc, format="json") # Set the new data catalog for a catalog record and store the catalog record cr = CatalogRecord.objects.get(pk=1) @@ -737,51 +859,93 @@ def test_read_catalog_record_search_by_data_catalog_id(self): cr.force_save() # Verify - response = self.client.get('/rest/datasets?data_catalog={0}'.format(dc_id)) + response = self.client.get("/rest/datasets?data_catalog={0}".format(dc_id)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) - self.assertEqual(response.data['results'][0]['data_catalog']['identifier'], dc_id) + self.assertEqual(len(response.data["results"]), 1) + self.assertEqual(response.data["results"][0]["data_catalog"]["identifier"], dc_id) + + def test_filter_by_projects_for_service(self): + """Filter datasets by projects. Services can access all projects.""" + user = settings.API_TEST_USER + self._use_http_authorization(username=user["username"], password=user["password"]) + response = self.client.get("/rest/datasets?projects=project_x&pagination=false") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 14) + + response = self.client.get("/rest/datasets?projects=research_project_112&pagination=false") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + response = self.client.get( + "/rest/datasets?projects=research_project_112,project_x&pagination=false" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 15) + + response = self.client.get("/rest/datasets?projects=no_datasets_here&pagination=false") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 0) + + @responses.activate + def test_filter_by_projects_for_end_user(self): + """Filter datasets by projects. End users can only access their own projects.""" + self._mock_token_validation_succeeds() + self._use_http_authorization( + method="bearer", + token={"group_names": ["IDA01:project_x", "IDA01:no_datasets_here"], "CSCUserName": "testi"} + ) + + response = self.client.get("/rest/datasets?projects=project_x&pagination=false") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 14) + + response = self.client.get("/rest/datasets?projects=no_datasets_here&pagination=false") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 0) + + response = self.client.get("/rest/datasets?projects=research_project_112&pagination=false") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_filter_by_deprecated(self): cr = CatalogRecord.objects.get(pk=1) cr.deprecated = True cr.force_save() - response = self.client.get('/rest/datasets?deprecated=true') + response = self.client.get("/rest/datasets?deprecated=true") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1, response.data['results']) - self.assertTrue(response.data['results'][0]['deprecated'], response.data) + self.assertEqual(len(response.data["results"]), 1, response.data["results"]) + self.assertTrue(response.data["results"][0]["deprecated"], response.data) - response = self.client.get('/rest/datasets?deprecated=false') + response = self.client.get("/rest/datasets?deprecated=false") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['results'][0]['id'], 2, response.data) - self.assertFalse(response.data['results'][0]['deprecated'], response.data) + self.assertEqual(response.data["results"][0]["id"], 2, response.data) + self.assertFalse(response.data["results"][0]["deprecated"], response.data) - response = self.client.get('/rest/datasets?deprecated=badbool') + response = self.client.get("/rest/datasets?deprecated=badbool") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_filter_by_api_version(self): # update one dataset to v2 so that we get non-zero values for that as well - response = self.client.put('/rest/v2/datasets', [self.cr_from_test_data], format='json') + response = self.client.put("/rest/v2/datasets", [self.cr_from_test_data], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) # both models return correct count because v2 model is just a proxy - v1_count = CatalogRecord.objects.filter(api_meta__contains={'version': 1}).count() + v1_count = CatalogRecord.objects.filter(api_meta__contains={"version": 1}).count() - v2_count = CatalogRecord.objects.filter(api_meta__contains={'version': 2}).count() + v2_count = CatalogRecord.objects.filter(api_meta__contains={"version": 2}).count() - response = self.client.get('/rest/datasets?api_version=1') + response = self.client.get("/rest/datasets?api_version=1") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(v1_count, response.data['count'], response.data) + self.assertEqual(v1_count, response.data["count"], response.data) - response = self.client.get('/rest/datasets?api_version=2') + response = self.client.get("/rest/datasets?api_version=2") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(v2_count, response.data['count'], response.data) + self.assertEqual(v2_count, response.data["count"], response.data) - response = self.client.get('/rest/datasets?api_version=not_int') + response = self.client.get("/rest/datasets?api_version=not_int") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('not an integer' in response.data['api_version'][0], response.data) + self.assertTrue("not an integer" in response.data["api_version"][0], response.data) def test_filter_by_legacy(self): self.create_legacy_data_catalogs() @@ -791,15 +955,16 @@ def test_filter_by_legacy(self): non_legacy_count = CatalogRecord.objects.exclude( data_catalog__catalog_json__identifier__in=settings.LEGACY_CATALOGS ).count() - response = self.client.get('/rest/datasets') + response = self.client.get("/rest/datasets") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(non_legacy_count, response.data['count'], response.data) + self.assertEqual(non_legacy_count, response.data["count"], response.data) # legacy datasets can be included with a parameter count_all = CatalogRecord.objects.count() - response = self.client.get('/rest/datasets?include_legacy') + response = self.client.get("/rest/datasets?include_legacy") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(count_all, response.data['count'], response.data) + self.assertEqual(count_all, response.data["count"], response.data) + class CatalogRecordApiReadXMLTransformationTests(CatalogRecordApiReadCommon): @@ -809,52 +974,61 @@ class CatalogRecordApiReadXMLTransformationTests(CatalogRecordApiReadCommon): def _create_dataset_with_doi(self): # Create ida data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) + dc = self._get_object_from_test_data("datacatalog", requested_index=0) dc_id = settings.IDA_DATA_CATALOG_IDENTIFIER - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/datacatalogs', dc, format="json") + dc["catalog_json"]["identifier"] = dc_id + self.client.post("/rest/datacatalogs", dc, format="json") # Create new cr by requesting a doi identifier - cr_json = self.client.get('/rest/datasets/1').data - cr_json.pop('preservation_identifier', None) - cr_json.pop('identifier') - cr_json['research_dataset'].pop('preferred_identifier', None) - cr_json['research_dataset']['publisher'] = {'@type': 'Organization', 'name': {'und': 'Testaaja'}} - cr_json['research_dataset']['issued'] = '2010-01-01' - cr_json['data_catalog'] = dc_id - response = self.client.post('/rest/datasets?pid_type=doi', cr_json, format="json") + cr_json = self.client.get("/rest/datasets/1").data + cr_json.pop("preservation_identifier", None) + cr_json.pop("identifier") + cr_json["research_dataset"].pop("preferred_identifier", None) + cr_json["research_dataset"]["publisher"] = { + "@type": "Organization", + "name": {"und": "Testaaja"}, + } + cr_json["research_dataset"]["issued"] = "2010-01-01" + cr_json["data_catalog"] = dc_id + response = self.client.post("/rest/datasets?pid_type=doi", cr_json, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) return response.data def test_read_dataset_xml_format_metax(self): - response = self.client.get('/rest/datasets/1?dataset_format=metax') - self._check_dataset_xml_format_response(response, '%s' % doi[len('doi:'):] in response.data, - True, response.data) + self.assertEqual( + '%s' % doi[len("doi:") :] in response.data, + True, + response.data, + ) def test_read_dataset_format_datacite_odd_lang_abbrevation(self): cr = CatalogRecord.objects.get(pk=1) - cr.research_dataset['publisher'] = {'@type': 'Organization', 'name': {'zk': 'Testiorganisaatio'}} + cr.research_dataset["publisher"] = { + "@type": "Organization", + "name": {"zk": "Testiorganisaatio"}, + } cr.force_save() - response = self.client.get('/rest/datasets/1?dataset_format=fairdata_datacite') + response = self.client.get("/rest/datasets/1?dataset_format=fairdata_datacite") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) def test_read_dataset_format_dummy_datacite_doi(self): @@ -863,28 +1037,44 @@ def test_read_dataset_format_dummy_datacite_doi(self): and identifier value prefixed with 10.0/. If a real DOI is available in the dataset, then dummy should NOT be returned. """ - pid = self.client.get('/rest/datasets/12').data['research_dataset']['preferred_identifier'] - self.assertEqual(pid.startswith('doi:'), False, pid) + pid = self.client.get("/rest/datasets/12").data["research_dataset"]["preferred_identifier"] + self.assertEqual(pid.startswith("doi:"), False, pid) - for dataset_format in ['datacite', 'fairdata_datacite']: - response = self.client.get('/rest/datasets/12?dataset_format=%s&dummy_doi=true' % dataset_format) + for dataset_format in ["datacite", "fairdata_datacite"]: + response = self.client.get( + "/rest/datasets/12?dataset_format=%s&dummy_doi=true" % dataset_format + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('' in response.data, True, response.data) - self.assertEqual('10.0/%s' % pid in response.data, True, response.data) + self.assertEqual( + '' in response.data, + True, + response.data, + ) + self.assertEqual("10.0/%s" % pid in response.data, True, response.data) # ensure if a real doi exists, then dummy should never be returned cr = self._create_dataset_with_doi() - response = self.client.get('/rest/datasets/%d?dataset_format=datacite&dummy_doi=true' % cr['id']) + response = self.client.get( + "/rest/datasets/%d?dataset_format=datacite&dummy_doi=true" % cr["id"] + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual('' in response.data, True, response.data) - self.assertEqual(cr['preservation_identifier'][len('doi:'):] in response.data, True, response.data) - self.assertEqual('10.0/%s' % pid in response.data, False, response.data) + self.assertEqual( + cr["preservation_identifier"][len("doi:") :] in response.data, + True, + response.data, + ) + self.assertEqual("10.0/%s" % pid in response.data, False, response.data) def _check_dataset_xml_format_response(self, response, element_name): self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('content-type' in response._headers, True, response._headers) - self.assertEqual('application/xml' in response._headers['content-type'][1], True, response._headers) - self.assertEqual(' 6) @@ -937,35 +1129,64 @@ class CatalogRecordApiReadPopulateFileInfoTests(CatalogRecordApiReadCommon): def test_file_details_populated(self): # without the flag nothing should happen - response = self.client.get('/rest/datasets/1') - self.assertEqual(all('details' not in f for f in response.data['research_dataset']['files']), True) + response = self.client.get("/rest/datasets/1") + self.assertEqual( + all("details" not in f for f in response.data["research_dataset"]["files"]), + True, + ) - response = self.client.get('/rest/datasets/1?file_details') + response = self.client.get("/rest/datasets/1?file_details") self.assertEqual(response.status_code, status.HTTP_200_OK) # check all fiels have the extra key 'details', and all details have the key 'identifier'. # presumably the details were then filled in. - self.assertEqual(all('details' in f for f in response.data['research_dataset']['files']), True) - self.assertEqual(all('identifier' in f['details'] for f in response.data['research_dataset']['files']), True) + self.assertEqual( + all("details" in f for f in response.data["research_dataset"]["files"]), + True, + ) + self.assertEqual( + all("identifier" in f["details"] for f in response.data["research_dataset"]["files"]), + True, + ) def test_directory_details_populated(self): # id 11 is one of the example datasets with full details. they should have a couple # of directories attached. CatalogRecord.objects.get(pk=11).calculate_directory_byte_sizes_and_file_counts() - response = self.client.get('/rest/datasets/11?file_details') + response = self.client.get("/rest/datasets/11?file_details") self.assertEqual(response.status_code, status.HTTP_200_OK) # check all dirs have the extra key 'details', and all details have the key 'identifier'. # presumably the details were then filled in. - self.assertEqual(all('details' in f for f in response.data['research_dataset']['directories']), True) - self.assertEqual(all('identifier' in f['details'] for f in response.data['research_dataset']['directories']), - True) + self.assertEqual( + all("details" in f for f in response.data["research_dataset"]["directories"]), + True, + ) + self.assertEqual( + all( + "identifier" in f["details"] + for f in response.data["research_dataset"]["directories"] + ), + True, + ) # additionally check that file counts and total byte sizes are as expected - self.assertEqual(response.data['research_dataset']['directories'][0]['details']['byte_size'], 21000) - self.assertEqual(response.data['research_dataset']['directories'][1]['details']['byte_size'], 21000) - self.assertEqual(response.data['research_dataset']['directories'][0]['details']['file_count'], 20) - self.assertEqual(response.data['research_dataset']['directories'][1]['details']['file_count'], 20) + self.assertEqual( + response.data["research_dataset"]["directories"][0]["details"]["byte_size"], + 21000, + ) + self.assertEqual( + response.data["research_dataset"]["directories"][1]["details"]["byte_size"], + 21000, + ) + self.assertEqual( + response.data["research_dataset"]["directories"][0]["details"]["file_count"], + 20, + ) + self.assertEqual( + response.data["research_dataset"]["directories"][1]["details"]["file_count"], + 20, + ) def test_file_details_for_deprecated_datasets(self): """ @@ -978,12 +1199,13 @@ def test_file_details_for_deprecated_datasets(self): cr = CatalogRecord.objects.get(pk=11) file_identifiers = File.objects.filter( - project_identifier=cr.files.all()[0].project_identifier).values_list('identifier', flat=True) + project_identifier=cr.files.all()[0].project_identifier + ).values_list("identifier", flat=True) - response = self.client.delete('/rest/files', data=file_identifiers, format='json') + response = self.client.delete("/rest/files", data=file_identifiers, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.get('/rest/datasets/11?file_details', format='json') + response = self.client.get("/rest/datasets/11?file_details", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -1000,21 +1222,21 @@ def test_returns_all_details_for_open_catalog_record_if_no_authorization(self): # Verify all file and dir details info is returned for open cr /rest/datasets/?file_details without # authorization - self._assert_ok(open_cr_json, 'no') + self._assert_ok(open_cr_json, "no") def test_returns_all_details_for_login_catalog_record_if_no_authorization(self): open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() # Verify all file and dir details info is returned for open cr /rest/datasets/?file_details without # authorization - self._assert_ok(open_cr_json, 'no') + self._assert_ok(open_cr_json, "no") def test_returns_all_details_for_open_catalog_record_if_service_authorization(self): open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() # Verify all file and dir details info is returned for open cr /rest/datasets/?file_details with # service authorization - self._assert_ok(open_cr_json, 'service') + self._assert_ok(open_cr_json, "service") @responses.activate def test_returns_all_details_for_open_catalog_record_if_owner_authorization(self): @@ -1023,88 +1245,104 @@ def test_returns_all_details_for_open_catalog_record_if_owner_authorization(self # Verify all file and dir details info is returned for open owner-owned cr /rest/datasets/?file_details with # owner authorization - self._assert_ok(open_cr_json, 'owner') + self._assert_ok(open_cr_json, "owner") - def test_returns_all_details_for_restricted_catalog_record_if_service_authorization(self): + def test_returns_all_details_for_restricted_catalog_record_if_service_authorization( + self, + ): restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() # Verify all file and dir details info is returned for restricted cr /rest/datasets/?file_details with # service authorization - self._assert_ok(restricted_cr_json, 'service') + self._assert_ok(restricted_cr_json, "service") @responses.activate - def test_returns_all_details_for_restricted_catalog_record_if_owner_authorization(self): + def test_returns_all_details_for_restricted_catalog_record_if_owner_authorization( + self, + ): self.create_end_user_data_catalogs() - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details(True) + restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details( + True + ) # Verify all file and dir details info is returned for restricted owner-owned cr # /rest/datasets/?file_details with owner authorization - self._assert_ok(restricted_cr_json, 'owner') + self._assert_ok(restricted_cr_json, "owner") - def test_returns_all_details_for_embargoed_catalog_record_if_available_reached_and_no_authorization(self): - available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) + def test_returns_all_details_for_embargoed_catalog_record_if_available_reached_and_no_authorization( + self, + ): + available_embargoed_cr_json = ( + self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) + ) # Verify all file and dir details info is returned for embargoed cr /rest/datasets/?file_details when # embargo date has been reached without authorization - self._assert_ok(available_embargoed_cr_json, 'no') + self._assert_ok(available_embargoed_cr_json, "no") # THE FORBIDDEN TESTS - def test_returns_limited_info_for_restricted_catalog_record_if_no_authorization(self): + def test_returns_limited_info_for_restricted_catalog_record_if_no_authorization( + self, + ): restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() # Verify limited file and dir info for restricted cr /rest/datasets/?file_details without authorization - self._assert_limited_or_no_file_dir_info(restricted_cr_json, 'no') + self._assert_limited_or_no_file_dir_info(restricted_cr_json, "no") - def test_returns_limited_info_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization(self): - not_available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(False) + def test_returns_limited_info_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization( + self, + ): + not_available_embargoed_cr_json = ( + self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(False) + ) # Verify limited file and dir info for embargoed cr /rest/datasets/?file_details when embargo date has not # been reached without authorization - self._assert_limited_or_no_file_dir_info(not_available_embargoed_cr_json, 'no') + self._assert_limited_or_no_file_dir_info(not_available_embargoed_cr_json, "no") def _assert_limited_or_no_file_dir_info(self, cr_json, credentials_type): self._set_http_authorization(credentials_type) - file_amt = len(cr_json['research_dataset']['files']) - dir_amt = len(cr_json['research_dataset']['directories']) - pk = cr_json['id'] + file_amt = len(cr_json["research_dataset"]["files"]) + dir_amt = len(cr_json["research_dataset"]["directories"]) + pk = cr_json["id"] - response = self.client.get('/rest/datasets/{0}?file_details'.format(pk)) + response = self.client.get("/rest/datasets/{0}?file_details".format(pk)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['files']), file_amt) - self.assertEqual(len(response.data['research_dataset']['directories']), dir_amt) + self.assertEqual(len(response.data["research_dataset"]["files"]), file_amt) + self.assertEqual(len(response.data["research_dataset"]["directories"]), dir_amt) - for f in response.data['research_dataset']['files']: - self.assertTrue('details' in f) + for f in response.data["research_dataset"]["files"]: + self.assertTrue("details" in f) # The below assert is a bit arbitrary - self.assertTrue(len(f['details'].keys()) < 5) - for d in response.data['research_dataset']['directories']: - self.assertTrue('details' in d) + self.assertTrue(len(f["details"].keys()) < 5) + for d in response.data["research_dataset"]["directories"]: + self.assertTrue("details" in d) # The below assert is a bit arbitrary - self.assertTrue(len(d['details'].keys()) < 5) + self.assertTrue(len(d["details"].keys()) < 5) def _assert_ok(self, cr_json, credentials_type): self._set_http_authorization(credentials_type) - file_amt = len(cr_json['research_dataset']['files']) - dir_amt = len(cr_json['research_dataset']['directories']) - pk = cr_json['id'] + file_amt = len(cr_json["research_dataset"]["files"]) + dir_amt = len(cr_json["research_dataset"]["directories"]) + pk = cr_json["id"] - response = self.client.get('/rest/datasets/{0}?file_details'.format(pk)) + response = self.client.get("/rest/datasets/{0}?file_details".format(pk)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['files']), file_amt) - self.assertEqual(len(response.data['research_dataset']['directories']), dir_amt) + self.assertEqual(len(response.data["research_dataset"]["files"]), file_amt) + self.assertEqual(len(response.data["research_dataset"]["directories"]), dir_amt) - for f in response.data['research_dataset']['files']: - self.assertTrue('details' in f) + for f in response.data["research_dataset"]["files"]: + self.assertTrue("details" in f) # The below assert is a bit arbitrary - self.assertTrue(len(f['details'].keys()) > 5) - for d in response.data['research_dataset']['directories']: - self.assertTrue('details' in d) + self.assertTrue(len(f["details"].keys()) > 5) + for d in response.data["research_dataset"]["directories"]: + self.assertTrue("details" in d) # The below assert is a bit arbitrary - self.assertTrue(len(d['details'].keys()) > 5) + self.assertTrue(len(d["details"].keys()) > 5) class CatalogRecordApiReadFiles(CatalogRecordApiReadCommon): @@ -1115,7 +1353,7 @@ class CatalogRecordApiReadFiles(CatalogRecordApiReadCommon): def test_get_files(self): file_count = CatalogRecord.objects.get(pk=1).files.count() - response = self.client.get('/rest/datasets/1/files') + response = self.client.get("/rest/datasets/1/files") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), file_count) @@ -1123,19 +1361,19 @@ def test_get_files_specified_fields_only(self): """ Test use of query parameter ?file_fields=x,y,z """ - response = self.client.get('/rest/datasets/1/files?file_fields=identifier,file_path') + response = self.client.get("/rest/datasets/1/files?file_fields=identifier,file_path") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data[0].keys()), 2) - self.assertEqual('identifier' in response.data[0], True) - self.assertEqual('file_path' in response.data[0], True) + self.assertEqual("identifier" in response.data[0], True) + self.assertEqual("file_path" in response.data[0], True) def test_removed_query_param(self): """ Test use of query parameter removed_files=bool in /datasets/pid/files, which should return only deleted files. """ - response = self.client.get('/rest/datasets/1/files') - file_ids_before = set([ f['id'] for f in response.data ]) + response = self.client.get("/rest/datasets/1/files") + file_ids_before = set([f["id"] for f in response.data]) obj = File.objects.get(pk=1) obj.removed = True obj.force_save() @@ -1143,14 +1381,14 @@ def test_removed_query_param(self): obj2.removed = True obj2.force_save() - response = self.client.get('/rest/datasets/1/files') + response = self.client.get("/rest/datasets/1/files") self.assertEqual(response.status_code, status.HTTP_200_OK, response.content) self.assertEqual(len(response.data), 0) - response = self.client.get('/rest/datasets/1/files?removed_files=true') + response = self.client.get("/rest/datasets/1/files?removed_files=true") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), len(file_ids_before)) - self.assertEqual(file_ids_before, set([ f['id'] for f in response.data ])) + self.assertEqual(file_ids_before, set([f["id"] for f in response.data])) class CatalogRecordApiReadFilesAuthorization(CatalogRecordApiReadCommon): @@ -1164,13 +1402,13 @@ def test_returns_ok_for_open_catalog_record_if_no_authorization(self): open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() # Verify open dataset /rest/datasets//files returns all the files even without authorization - self._assert_ok(open_cr_json, 'no') + self._assert_ok(open_cr_json, "no") def test_returns_ok_for_open_catalog_record_if_service_authorization(self): open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() # Verify open dataset /rest/datasets//files returns all the files with service authorization - self._assert_ok(open_cr_json, 'service') + self._assert_ok(open_cr_json, "service") @responses.activate def test_returns_ok_for_open_catalog_record_if_owner_authorization(self): @@ -1178,29 +1416,35 @@ def test_returns_ok_for_open_catalog_record_if_owner_authorization(self): open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(True) # Verify open owner-owned dataset /rest/datasets//files returns all the files with owner authorization - self._assert_ok(open_cr_json, 'owner') + self._assert_ok(open_cr_json, "owner") def test_returns_ok_for_restricted_catalog_record_if_service_authorization(self): restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() # Verify restricted dataset /rest/datasets//files returns all the files with service authorization - self._assert_ok(restricted_cr_json, 'service') + self._assert_ok(restricted_cr_json, "service") @responses.activate def test_returns_ok_for_restricted_catalog_record_if_owner_authorization(self): self.create_end_user_data_catalogs() - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details(True) + restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details( + True + ) # Verify restricted owner-owned dataset /rest/datasets//files returns all the files with # owner authorization - self._assert_ok(restricted_cr_json, 'owner') + self._assert_ok(restricted_cr_json, "owner") - def test_returns_ok_for_embargoed_catalog_record_if_available_reached_and_no_authorization(self): - available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) + def test_returns_ok_for_embargoed_catalog_record_if_available_reached_and_no_authorization( + self, + ): + available_embargoed_cr_json = ( + self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) + ) # Verify restricted dataset /rest/datasets//files returns ok when embargo date has been reached without # authorization - self._assert_ok(available_embargoed_cr_json, 'no') + self._assert_ok(available_embargoed_cr_json, "no") # THE FORBIDDEN TESTS @@ -1208,25 +1452,31 @@ def test_returns_forbidden_for_restricted_catalog_record_if_no_authorization(sel restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() # Verify restricted dataset /rest/datasets//files returns forbidden without authorization - self._assert_forbidden(restricted_cr_json, 'no') + self._assert_forbidden(restricted_cr_json, "no") - def test_returns_forbidden_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization(self): - not_available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(False) + def test_returns_forbidden_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization( + self, + ): + not_available_embargoed_cr_json = ( + self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(False) + ) # Verify restricted dataset /rest/datasets//files returns forbidden when embargo date has not been reached - self._assert_forbidden(not_available_embargoed_cr_json, 'no') + self._assert_forbidden(not_available_embargoed_cr_json, "no") def _assert_forbidden(self, cr_json, credentials_type): - pk = cr_json['id'] + pk = cr_json["id"] self._set_http_authorization(credentials_type) - response = self.client.get('/rest/datasets/{0}/files'.format(pk)) + response = self.client.get("/rest/datasets/{0}/files".format(pk)) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def _assert_ok(self, cr_json, credentials_type): - pk = cr_json['id'] + pk = cr_json["id"] self._set_http_authorization(credentials_type) - rd = cr_json['research_dataset'] - file_amt = len(rd['files']) + sum(int(d['details']['file_count']) for d in rd['directories']) - response = self.client.get('/rest/datasets/{0}/files'.format(pk)) + rd = cr_json["research_dataset"] + file_amt = len(rd["files"]) + sum( + int(d["details"]["file_count"]) for d in rd["directories"] + ) + response = self.client.get("/rest/datasets/{0}/files".format(pk)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), file_amt) diff --git a/src/metax_api/tests/api/rest/base/views/datasets/write.py b/src/metax_api/tests/api/rest/base/views/datasets/write.py index 8c67daaf..7f7405d5 100755 --- a/src/metax_api/tests/api/rest/base/views/datasets/write.py +++ b/src/metax_api/tests/api/rest/base/views/datasets/write.py @@ -16,7 +16,14 @@ from rest_framework import status from rest_framework.test import APITestCase -from metax_api.models import AlternateRecordSet, CatalogRecord, Contract, DataCatalog, Directory, File +from metax_api.models import ( + AlternateRecordSet, + CatalogRecord, + Contract, + DataCatalog, + Directory, + File, +) from metax_api.models.catalog_record import ACCESS_TYPES from metax_api.services import ReferenceDataMixin as RDM from metax_api.services.redis_cache_service import RedisClient @@ -30,6 +37,7 @@ EXT_CATALOG = django_settings.EXT_DATA_CATALOG_IDENTIFIER DFT_CATALOG = django_settings.DFT_DATA_CATALOG_IDENTIFIER + class CatalogRecordApiWriteCommon(APITestCase, TestClassUtils): """ Common class for write tests, inherited by other write test classes @@ -39,19 +47,24 @@ def setUp(self): """ Reloaded for every test case """ - call_command('loaddata', test_data_file_path, verbosity=0) - catalog_record_from_test_data = self._get_object_from_test_data('catalogrecord') - self.preferred_identifier = catalog_record_from_test_data['research_dataset']['preferred_identifier'] - self.identifier = catalog_record_from_test_data['identifier'] - self.pk = catalog_record_from_test_data['id'] + call_command("loaddata", test_data_file_path, verbosity=0) + catalog_record_from_test_data = self._get_object_from_test_data("catalogrecord") + self.preferred_identifier = catalog_record_from_test_data["research_dataset"][ + "preferred_identifier" + ] + self.identifier = catalog_record_from_test_data["identifier"] + self.pk = catalog_record_from_test_data["id"] """ New data that is sent to the server for POST, PUT, PATCH requests. Modified slightly as approriate for different purposes """ self.cr_test_data = self._get_new_test_cr_data() - self.cr_test_data['research_dataset']['publisher'] = {'@type': 'Organization', 'name': {'und': 'Testaaja'}} - self.cr_test_data['research_dataset']['issued'] = '2010-01-01' + self.cr_test_data["research_dataset"]["publisher"] = { + "@type": "Organization", + "name": {"und": "Testaaja"}, + } + self.cr_test_data["research_dataset"]["issued"] = "2010-01-01" self.cr_att_test_data = self._get_new_test_cr_data(cr_index=14, dc_index=5) self.cr_test_data_new_identifier = self._get_new_test_cr_data_with_updated_identifier() @@ -61,11 +74,13 @@ def setUp(self): self._use_http_authorization() def update_record(self, record): - return self.client.put('/rest/datasets/%d' % record['id'], record, format="json") + return self.client.put("/rest/datasets/%d" % record["id"], record, format="json") def get_next_version(self, record): - self.assertEqual('next_dataset_version' in record, True) - response = self.client.get('/rest/datasets/%d' % record['next_dataset_version']['id'], format="json") + self.assertEqual("next_dataset_version" in record, True) + response = self.client.get( + "/rest/datasets/%d" % record["next_dataset_version"]["id"], format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) return response.data @@ -78,45 +93,62 @@ def get_next_version(self, record): # def _get_new_test_cr_data(self, cr_index=0, dc_index=0, c_index=0): - dc = self._get_object_from_test_data('datacatalog', requested_index=dc_index) - catalog_record_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=cr_index) - - if dc['catalog_json']['research_dataset_schema'] == 'ida' and \ - 'remote_resources' in catalog_record_from_test_data['research_dataset']: - self.fail("Cannot generate the requested test catalog record since requested data catalog is indicates ida " - "schema and the requested catalog record is having remote resources, which is not allowed") - - if dc['catalog_json']['research_dataset_schema'] == 'att' and \ - ('files' in catalog_record_from_test_data['research_dataset'] or - 'directories' in catalog_record_from_test_data['research_dataset']): - self.fail("Cannot generate the requested test catalog record since requested data catalog is indicates att " - "schema and the requested catalog record is having files or directories, which is not allowed") - - catalog_record_from_test_data.update({ - "contract": self._get_object_from_test_data('contract', requested_index=c_index), - "data_catalog": dc - }) - catalog_record_from_test_data['research_dataset'].update({ - "creator": [{ - "@type": "Person", - "name": "Teppo Testaaja", - "member_of": { - "@type": "Organization", - "name": {"fi": "Mysterious Organization"} - } - }], - "curator": [{ - "@type": "Person", - "name": "Default Owner", - "member_of": { - "@type": "Organization", - "name": {"fi": "Mysterious Organization"} - } - }] - }) - catalog_record_from_test_data['research_dataset'].pop('preferred_identifier', None) - catalog_record_from_test_data['research_dataset'].pop('metadata_version_identifier', None) - catalog_record_from_test_data.pop('identifier', None) + dc = self._get_object_from_test_data("datacatalog", requested_index=dc_index) + catalog_record_from_test_data = self._get_object_from_test_data( + "catalogrecord", requested_index=cr_index + ) + + if ( + dc["catalog_json"]["research_dataset_schema"] == "ida" + and "remote_resources" in catalog_record_from_test_data["research_dataset"] + ): + self.fail( + "Cannot generate the requested test catalog record since requested data catalog is indicates ida " + "schema and the requested catalog record is having remote resources, which is not allowed" + ) + + if dc["catalog_json"]["research_dataset_schema"] == "att" and ( + "files" in catalog_record_from_test_data["research_dataset"] + or "directories" in catalog_record_from_test_data["research_dataset"] + ): + self.fail( + "Cannot generate the requested test catalog record since requested data catalog is indicates att " + "schema and the requested catalog record is having files or directories, which is not allowed" + ) + + catalog_record_from_test_data.update( + { + "contract": self._get_object_from_test_data("contract", requested_index=c_index), + "data_catalog": dc, + } + ) + catalog_record_from_test_data["research_dataset"].update( + { + "creator": [ + { + "@type": "Person", + "name": "Teppo Testaaja", + "member_of": { + "@type": "Organization", + "name": {"fi": "Mysterious Organization"}, + }, + } + ], + "curator": [ + { + "@type": "Person", + "name": "Default Owner", + "member_of": { + "@type": "Organization", + "name": {"fi": "Mysterious Organization"}, + }, + } + ], + } + ) + catalog_record_from_test_data["research_dataset"].pop("preferred_identifier", None) + catalog_record_from_test_data["research_dataset"].pop("metadata_version_identifier", None) + catalog_record_from_test_data.pop("identifier", None) return catalog_record_from_test_data def _get_new_test_cr_data_with_updated_identifier(self): @@ -130,28 +162,43 @@ def _get_new_full_test_ida_cr_data(self): """ Returns one of the fuller generated test datasets """ - catalog_record_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=11) - data_catalog_from_test_data = self._get_object_from_test_data('datacatalog', requested_index=0) - return self._get_new_full_test_cr_data(catalog_record_from_test_data, data_catalog_from_test_data) + catalog_record_from_test_data = self._get_object_from_test_data( + "catalogrecord", requested_index=11 + ) + data_catalog_from_test_data = self._get_object_from_test_data( + "datacatalog", requested_index=0 + ) + return self._get_new_full_test_cr_data( + catalog_record_from_test_data, data_catalog_from_test_data + ) def _get_new_full_test_att_cr_data(self): """ Returns one of the fuller generated test datasets """ - catalog_record_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=23) - data_catalog_from_test_data = self._get_object_from_test_data('datacatalog', requested_index=5) - return self._get_new_full_test_cr_data(catalog_record_from_test_data, data_catalog_from_test_data) + catalog_record_from_test_data = self._get_object_from_test_data( + "catalogrecord", requested_index=23 + ) + data_catalog_from_test_data = self._get_object_from_test_data( + "datacatalog", requested_index=5 + ) + return self._get_new_full_test_cr_data( + catalog_record_from_test_data, data_catalog_from_test_data + ) def _get_new_full_test_cr_data(self, cr_from_test_data, dc_from_test_data): - cr_from_test_data.update({ - "contract": self._get_object_from_test_data('contract', requested_index=0), - "data_catalog": dc_from_test_data - }) - cr_from_test_data['research_dataset'].pop('metadata_version_identifier') - cr_from_test_data['research_dataset'].pop('preferred_identifier') - cr_from_test_data.pop('identifier') + cr_from_test_data.update( + { + "contract": self._get_object_from_test_data("contract", requested_index=0), + "data_catalog": dc_from_test_data, + } + ) + cr_from_test_data["research_dataset"].pop("metadata_version_identifier") + cr_from_test_data["research_dataset"].pop("preferred_identifier") + cr_from_test_data.pop("identifier") return cr_from_test_data + class CatalogRecordApiWriteCreateTests(CatalogRecordApiWriteCommon): # # @@ -162,49 +209,58 @@ class CatalogRecordApiWriteCreateTests(CatalogRecordApiWriteCommon): # def test_issued_date_is_generated(self): - ''' Issued date is generated for all but harvested catalogs if it doesn't exists ''' + """ Issued date is generated for all but harvested catalogs if it doesn't exists """ dc = DataCatalog.objects.get(pk=2) - dc.catalog_json['identifier'] = IDA_CATALOG # Test with IDA catalog + dc.catalog_json["identifier"] = IDA_CATALOG # Test with IDA catalog dc.force_save() - self.cr_test_data['data_catalog'] = dc.catalog_json - self.cr_test_data['research_dataset'].pop('issued', None) + self.cr_test_data["data_catalog"] = dc.catalog_json + self.cr_test_data["research_dataset"].pop("issued", None) - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue('issued' in response.data['research_dataset'], response.data) + self.assertTrue("issued" in response.data["research_dataset"], response.data) def test_create_catalog_record(self): - self.cr_test_data['research_dataset']['preferred_identifier'] = 'this_should_be_overwritten' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = "this_should_be_overwritten" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('research_dataset' in response.data.keys(), True) - self.assertEqual('metadata_version_identifier' in response.data['research_dataset'], True, - 'metadata_version_identifier should have been generated') - self.assertEqual('preferred_identifier' in response.data['research_dataset'], True, - 'preferred_identifier should have been generated') + self.assertEqual("research_dataset" in response.data.keys(), True) + self.assertEqual( + "metadata_version_identifier" in response.data["research_dataset"], + True, + "metadata_version_identifier should have been generated", + ) + self.assertEqual( + "preferred_identifier" in response.data["research_dataset"], + True, + "preferred_identifier should have been generated", + ) self.assertNotEqual( - self.cr_test_data['research_dataset']['preferred_identifier'], - response.data['research_dataset']['preferred_identifier'], - 'in fairdata catalogs, user is not allowed to set preferred_identifier' + self.cr_test_data["research_dataset"]["preferred_identifier"], + response.data["research_dataset"]["preferred_identifier"], + "in fairdata catalogs, user is not allowed to set preferred_identifier", ) self.assertNotEqual( - response.data['research_dataset']['preferred_identifier'], - response.data['research_dataset']['metadata_version_identifier'], - 'preferred_identifier and metadata_version_identifier should be generated separately' + response.data["research_dataset"]["preferred_identifier"], + response.data["research_dataset"]["metadata_version_identifier"], + "preferred_identifier and metadata_version_identifier should be generated separately", + ) + cr = CatalogRecord.objects.get(pk=response.data["id"]) + self.assertEqual( + cr.date_created >= get_tz_aware_now_without_micros() - timedelta(seconds=5), + True, + "Timestamp should have been updated during object creation", ) - cr = CatalogRecord.objects.get(pk=response.data['id']) - self.assertEqual(cr.date_created >= get_tz_aware_now_without_micros() - timedelta(seconds=5), True, - 'Timestamp should have been updated during object creation') def test_create_catalog_record_as_harvester(self): - self.cr_test_data['research_dataset']['preferred_identifier'] = 'this_should_be_saved' - self.cr_test_data['data_catalog'] = 3 - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = "this_should_be_saved" + self.cr_test_data["data_catalog"] = 3 + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual( - self.cr_test_data['research_dataset']['preferred_identifier'], - response.data['research_dataset']['preferred_identifier'], - 'in harvested catalogs, user (the harvester) is allowed to set preferred_identifier' + self.cr_test_data["research_dataset"]["preferred_identifier"], + response.data["research_dataset"]["preferred_identifier"], + "in harvested catalogs, user (the harvester) is allowed to set preferred_identifier", ) def test_preferred_identifier_is_checked_also_from_deleted_records(self): @@ -216,81 +272,134 @@ def test_preferred_identifier_is_checked_also_from_deleted_records(self): # dc 3 happens to be harvested catalog, which allows setting pref id cr = CatalogRecord.objects.filter(data_catalog_id=3).first() - response = self.client.delete('/rest/datasets/%d' % cr.id) + response = self.client.delete("/rest/datasets/%d" % cr.id) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.cr_test_data['research_dataset']['preferred_identifier'] = cr.preferred_identifier - self.cr_test_data['data_catalog'] = 3 - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = cr.preferred_identifier + self.cr_test_data["data_catalog"] = 3 + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('already exists' in response.data['research_dataset'][0], True, response.data) + self.assertEqual( + "already exists" in response.data["research_dataset"][0], + True, + response.data, + ) def test_create_catalog_contract_string_identifier(self): - contract_identifier = Contract.objects.first().contract_json['identifier'] - self.cr_test_data['contract'] = contract_identifier - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + contract_identifier = Contract.objects.first().contract_json["identifier"] + self.cr_test_data["contract"] = contract_identifier + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['contract']['identifier'], contract_identifier, response.data) + self.assertEqual( + response.data["contract"]["identifier"], contract_identifier, response.data + ) def test_create_catalog_error_contract_string_identifier_not_found(self): - self.cr_test_data['contract'] = 'doesnotexist' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["contract"] = "doesnotexist" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") # self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, 'Should have raised 404 not found') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('contract' in response.data, True, 'Error should have been about contract not found') + self.assertEqual( + "contract" in response.data, + True, + "Error should have been about contract not found", + ) def test_create_catalog_record_json_validation_error_1(self): """ Ensure the json path of the error is returned along with other details """ - self.cr_test_data['research_dataset']["title"] = 1234456 - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["title"] = 1234456 + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(len(response.data), 2, 'there should be two errors (error_identifier is one of them)') - self.assertEqual('research_dataset' in response.data.keys(), True, - 'The error should concern the field research_dataset') - self.assertEqual('1234456 is not of type' in response.data['research_dataset'][0], True, response.data) - self.assertEqual('Json path: [\'title\']' in response.data['research_dataset'][0], True, response.data) + self.assertEqual( + len(response.data), + 2, + "there should be two errors (error_identifier is one of them)", + ) + self.assertEqual( + "research_dataset" in response.data.keys(), + True, + "The error should concern the field research_dataset", + ) + self.assertEqual( + "1234456 is not of type" in response.data["research_dataset"][0], + True, + response.data, + ) + self.assertEqual( + "Json path: ['title']" in response.data["research_dataset"][0], + True, + response.data, + ) def test_create_catalog_record_json_validation_error_2(self): """ Ensure the json path of the error is returned along with other details also in objects that are deeply nested """ - self.cr_test_data['research_dataset']['provenance'] = [{ - 'title': {'en': 'provenance title'}, - 'was_associated_with': [ - {'@type': 'Person', 'xname': 'seppo'} - ] - }] - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["provenance"] = [ + { + "title": {"en": "provenance title"}, + "was_associated_with": [{"@type": "Person", "xname": "seppo"}], + } + ] + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(len(response.data), 2, 'there should be two errors (error_identifier is one of them)') - self.assertEqual('research_dataset' in response.data.keys(), True, - 'The error should concern the field research_dataset') - self.assertEqual('is not valid' in response.data['research_dataset'][0], True, response.data) - self.assertEqual('was_associated_with' in response.data['research_dataset'][0], True, response.data) + self.assertEqual( + len(response.data), + 2, + "there should be two errors (error_identifier is one of them)", + ) + self.assertEqual( + "research_dataset" in response.data.keys(), + True, + "The error should concern the field research_dataset", + ) + self.assertEqual( + "is not valid" in response.data["research_dataset"][0], True, response.data + ) + self.assertEqual( + "was_associated_with" in response.data["research_dataset"][0], + True, + response.data, + ) def test_create_catalog_record_allowed_projects_ok(self): - response = self.client.post('/rest/datasets?allowed_projects=project_x', self.cr_test_data, format="json") + response = self.client.post( + "/rest/datasets?allowed_projects=project_x", + self.cr_test_data, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def test_create_catalog_record_allowed_projects_fail(self): # dataset file not in allowed projects - response = self.client.post('/rest/datasets?allowed_projects=no,permission', self.cr_test_data, format="json") + response = self.client.post( + "/rest/datasets?allowed_projects=no,permission", + self.cr_test_data, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) # ensure list is properly handled (separated by comma, end result should be list) - response = self.client.post('/rest/datasets?allowed_projects=no_good_project_x,another', - self.cr_test_data, format="json") + response = self.client.post( + "/rest/datasets?allowed_projects=no_good_project_x,another", + self.cr_test_data, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) # handle empty value - response = self.client.post('/rest/datasets?allowed_projects=', self.cr_test_data, format="json") + response = self.client.post( + "/rest/datasets?allowed_projects=", self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) # Other trickery - response = self.client.post('/rest/datasets?allowed_projects=,', self.cr_test_data, format="json") + response = self.client.post( + "/rest/datasets?allowed_projects=,", self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) # @@ -298,19 +407,25 @@ def test_create_catalog_record_allowed_projects_fail(self): # def test_create_catalog_record_list(self): - response = self.client.post('/rest/datasets', - [self.cr_test_data, self.cr_test_data_new_identifier], format="json") + response = self.client.post( + "/rest/datasets", + [self.cr_test_data, self.cr_test_data_new_identifier], + format="json", + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('object' in response.data['success'][0].keys(), True) - self.assertEqual(len(response.data['success']), 2) - self.assertEqual(len(response.data['failed']), 0) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual("failed" in response.data.keys(), True) + self.assertEqual("object" in response.data["success"][0].keys(), True) + self.assertEqual(len(response.data["success"]), 2) + self.assertEqual(len(response.data["failed"]), 0) def test_create_catalog_record_list_error_one_fails(self): - self.cr_test_data['research_dataset']["title"] = 1234456 - response = self.client.post('/rest/datasets', - [self.cr_test_data, self.cr_test_data_new_identifier], format="json") + self.cr_test_data["research_dataset"]["title"] = 1234456 + response = self.client.post( + "/rest/datasets", + [self.cr_test_data, self.cr_test_data_new_identifier], + format="json", + ) """ List response looks like @@ -329,43 +444,41 @@ def test_create_catalog_record_list_error_one_fails(self): } """ self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('object' in response.data['failed'][0].keys(), True) - self.assertEqual('research_dataset' in response.data['failed'][0]['errors'], True, response.data) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual("failed" in response.data.keys(), True) + self.assertEqual("object" in response.data["failed"][0].keys(), True) + self.assertEqual( + "research_dataset" in response.data["failed"][0]["errors"], + True, + response.data, + ) self.assertEqual( - '1234456 is not of type' in response.data['failed'][0]['errors']['research_dataset'][0], + "1234456 is not of type" in response.data["failed"][0]["errors"]["research_dataset"][0], True, - response.data + response.data, ) self.assertEqual( - 'Json path: [\'title\']' in response.data['failed'][0]['errors']['research_dataset'][0], + "Json path: ['title']" in response.data["failed"][0]["errors"]["research_dataset"][0], True, - response.data + response.data, ) def test_create_catalog_record_list_error_all_fail(self): # data catalog is a required field, should fail - self.cr_test_data['data_catalog'] = None - self.cr_test_data_new_identifier['data_catalog'] = None + self.cr_test_data["data_catalog"] = None + self.cr_test_data_new_identifier["data_catalog"] = None - response = self.client.post('/rest/datasets', - [self.cr_test_data, self.cr_test_data_new_identifier], format="json") + response = self.client.post( + "/rest/datasets", + [self.cr_test_data, self.cr_test_data_new_identifier], + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('object' in response.data['failed'][0].keys(), True) - self.assertEqual(len(response.data['success']), 0) - self.assertEqual(len(response.data['failed']), 2) - - def test_create_catalog_record_editor_field_is_optional(self): - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - new = response.data - new['research_dataset']['title']['en'] = 'updated title' - new.pop('editor') - response = self.client.put('/rest/datasets/%d' % new['id'], new, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual("failed" in response.data.keys(), True) + self.assertEqual("object" in response.data["failed"][0].keys(), True) + self.assertEqual(len(response.data["success"]), 0) + self.assertEqual(len(response.data["failed"]), 2) def test_parameter_migration_override_preferred_identifier_when_creating(self): """ @@ -373,11 +486,13 @@ def test_parameter_migration_override_preferred_identifier_when_creating(self): permitted. Using the optional query parameter ?migration_override=bool a custom preferred_identifier can be passed. """ - custom_pid = 'custom-pid-value' - self.cr_test_data['research_dataset']['preferred_identifier'] = custom_pid - response = self.client.post('/rest/datasets?migration_override', self.cr_test_data, format="json") + custom_pid = "custom-pid-value" + self.cr_test_data["research_dataset"]["preferred_identifier"] = custom_pid + response = self.client.post( + "/rest/datasets?migration_override", self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], custom_pid) + self.assertEqual(response.data["research_dataset"]["preferred_identifier"], custom_pid) def test_parameter_migration_override_no_preferred_identifier_when_creating(self): """ @@ -385,47 +500,61 @@ def test_parameter_migration_override_no_preferred_identifier_when_creating(self permitted. Using the optional query parameter ?migration_override=bool a custom preferred_identifier can be passed. """ - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/datasets?migration_override', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post( + "/rest/datasets?migration_override", self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue(len(response.data['research_dataset']['preferred_identifier']) > 0) + self.assertTrue(len(response.data["research_dataset"]["preferred_identifier"]) > 0) - self.cr_test_data['research_dataset'].pop('preferred_identifier', None) - response = self.client.post('/rest/datasets?migration_override', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"].pop("preferred_identifier", None) + response = self.client.post( + "/rest/datasets?migration_override", self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue(len(response.data['research_dataset']['preferred_identifier']) > 0) + self.assertTrue(len(response.data["research_dataset"]["preferred_identifier"]) > 0) def test_create_catalog_record_using_pid_type(self): # Test with pid_type = urn - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/datasets?pid_type=urn', self.cr_test_data, format="json") - self.assertTrue(response.data['research_dataset']['preferred_identifier'].startswith('urn:')) + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post("/rest/datasets?pid_type=urn", self.cr_test_data, format="json") + self.assertTrue( + response.data["research_dataset"]["preferred_identifier"].startswith("urn:") + ) # Test with pid_type = doi AND not ida catalog - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/datasets?pid_type=doi', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post("/rest/datasets?pid_type=doi", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # Create ida data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) + dc = self._get_object_from_test_data("datacatalog", requested_index=0) dc_id = IDA_CATALOG - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/datacatalogs', dc, format="json") + dc["catalog_json"]["identifier"] = dc_id + self.client.post("/rest/datacatalogs", dc, format="json") # Test with pid_type = doi AND ida catalog - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - self.cr_test_data['data_catalog'] = IDA_CATALOG - response = self.client.post('/rest/datasets?pid_type=doi', self.cr_test_data, format="json") - self.assertTrue(response.data['research_dataset']['preferred_identifier'].startswith('doi:10.')) + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + self.cr_test_data["data_catalog"] = IDA_CATALOG + response = self.client.post("/rest/datasets?pid_type=doi", self.cr_test_data, format="json") + self.assertTrue( + response.data["research_dataset"]["preferred_identifier"].startswith("doi:10.") + ) # Test with pid_type = not_known - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/datasets?pid_type=not_known', self.cr_test_data, format="json") - self.assertTrue(response.data['research_dataset']['preferred_identifier'].startswith('urn:')) + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post( + "/rest/datasets?pid_type=not_known", self.cr_test_data, format="json" + ) + self.assertTrue( + response.data["research_dataset"]["preferred_identifier"].startswith("urn:") + ) # Test without pid_type - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") - self.assertTrue(response.data['research_dataset']['preferred_identifier'].startswith('urn:')) + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") + self.assertTrue( + response.data["research_dataset"]["preferred_identifier"].startswith("urn:") + ) class CatalogRecordApiWriteIdentifierUniqueness(CatalogRecordApiWriteCommon): @@ -444,65 +573,91 @@ class CatalogRecordApiWriteIdentifierUniqueness(CatalogRecordApiWriteCommon): # create operations # - def test_create_catalog_record_error_preferred_identifier_cant_be_metadata_version_identifier(self): + def test_create_catalog_record_error_preferred_identifier_cant_be_metadata_version_identifier( + self, + ): """ preferred_identifier can never be the same as a metadata_version_identifier in another cr, in any catalog. """ - existing_metadata_version_identifier = CatalogRecord.objects.get(pk=1).metadata_version_identifier - self.cr_test_data['research_dataset']['preferred_identifier'] = existing_metadata_version_identifier + existing_metadata_version_identifier = CatalogRecord.objects.get( + pk=1 + ).metadata_version_identifier + self.cr_test_data["research_dataset"][ + "preferred_identifier" + ] = existing_metadata_version_identifier # setting preferred_identifier is only allowed in harvested catalogs. - self.cr_test_data['data_catalog'] = 3 + self.cr_test_data["data_catalog"] = 3 - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('research_dataset' in response.data.keys(), True, - 'The error should be about an error in research_dataset') + self.assertEqual( + "research_dataset" in response.data.keys(), + True, + "The error should be about an error in research_dataset", + ) # the error message should clearly state that the value of preferred_identifier appears in the # field metadata_version_identifier in another record, therefore two asserts - self.assertEqual('preferred_identifier' in response.data['research_dataset'][0], True, - 'The error should be about metadata_version_identifier existing with this identifier') - self.assertEqual('metadata_version_identifier' in response.data['research_dataset'][0], True, - 'The error should be about metadata_version_identifier existing with this identifier') + self.assertEqual( + "preferred_identifier" in response.data["research_dataset"][0], + True, + "The error should be about metadata_version_identifier existing with this identifier", + ) + self.assertEqual( + "metadata_version_identifier" in response.data["research_dataset"][0], + True, + "The error should be about metadata_version_identifier existing with this identifier", + ) - def test_create_catalog_record_error_preferred_identifier_exists_in_same_catalog(self): + def test_create_catalog_record_error_preferred_identifier_exists_in_same_catalog( + self, + ): """ preferred_identifier already existing in the same data catalog is an error """ - self.cr_test_data['research_dataset']['preferred_identifier'] = 'pid_by_harvester' - self.cr_test_data['data_catalog'] = 3 - cr_1 = self.client.post('/rest/datasets', self.cr_test_data, format="json").data + self.cr_test_data["research_dataset"]["preferred_identifier"] = "pid_by_harvester" + self.cr_test_data["data_catalog"] = 3 + cr_1 = self.client.post("/rest/datasets", self.cr_test_data, format="json").data - self.cr_test_data['research_dataset']['preferred_identifier'] = \ - cr_1['research_dataset']['preferred_identifier'] + self.cr_test_data["research_dataset"]["preferred_identifier"] = cr_1["research_dataset"][ + "preferred_identifier" + ] - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('research_dataset' in response.data.keys(), True, - 'The error should be about an error in research_dataset') - self.assertEqual('preferred_identifier' in response.data['research_dataset'][0], True, - 'The error should be about preferred_identifier already existing') + self.assertEqual( + "research_dataset" in response.data.keys(), + True, + "The error should be about an error in research_dataset", + ) + self.assertEqual( + "preferred_identifier" in response.data["research_dataset"][0], + True, + "The error should be about preferred_identifier already existing", + ) def test_create_catalog_record_preferred_identifier_exists_in_another_catalog(self): """ preferred_identifier existing in another data catalog is not an error. """ unique_identifier = self._set_preferred_identifier_to_record(pk=1, catalog_id=1) - self.cr_test_data['research_dataset']['preferred_identifier'] = unique_identifier + self.cr_test_data["research_dataset"]["preferred_identifier"] = unique_identifier # different catalog, should be OK (not ATT catalog, so preferred_identifier being saved # can exist in other catalogs) - self.cr_test_data['data_catalog'] = 3 + self.cr_test_data["data_catalog"] = 3 - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # # update operations # - def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_1(self): + def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_1( + self, + ): """ preferred_identifier existing in another data catalog is not an error. @@ -515,13 +670,15 @@ def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_1( cr.data_catalog_id = 3 cr.save() - data = self.client.get('/rest/datasets/3').data - data['research_dataset']['preferred_identifier'] = unique_identifier + data = self.client.get("/rest/datasets/3").data + data["research_dataset"]["preferred_identifier"] = unique_identifier - response = self.client.patch('/rest/datasets/3', data, format="json") + response = self.client.patch("/rest/datasets/3", data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_2(self): + def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_2( + self, + ): """ preferred_identifier existing in another data catalog is not an error. @@ -534,14 +691,16 @@ def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_2( """ unique_identifier = self._set_preferred_identifier_to_record(pk=1, catalog_id=1) - data = self.client.get('/rest/datasets/3').data - data['research_dataset']['preferred_identifier'] = unique_identifier - data['data_catalog'] = 3 + data = self.client.get("/rest/datasets/3").data + data["research_dataset"]["preferred_identifier"] = unique_identifier + data["data_catalog"] = 3 - response = self.client.patch('/rest/datasets/3', data, format="json") + response = self.client.patch("/rest/datasets/3", data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, data) - def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_3(self): + def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_3( + self, + ): """ preferred_identifier already existing in the same data catalog is an error, in other catalogs than ATT: Harvester or other catalogs cant contain same @@ -558,28 +717,36 @@ def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_3( # setup the record in db which will cause conflict unique_identifier = self._set_preferred_identifier_to_record(pk=3, catalog_id=3) - data = {'research_dataset': self.cr_test_data['research_dataset']} - data['research_dataset']['preferred_identifier'] = unique_identifier - data['data_catalog'] = 3 + data = {"research_dataset": self.cr_test_data["research_dataset"]} + data["research_dataset"]["preferred_identifier"] = unique_identifier + data["data_catalog"] = 3 - response = self.client.patch('/rest/datasets/2', data, format="json") + response = self.client.patch("/rest/datasets/2", data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('preferred_identifier' in response.data['research_dataset'][0], True, - 'The error should be about preferred_identifier already existing') + self.assertEqual( + "preferred_identifier" in response.data["research_dataset"][0], + True, + "The error should be about preferred_identifier already existing", + ) def test_remote_doi_dataset_is_validated_against_datacite_format(self): # Remote input DOI ids need to take datasets for datacite validation - cr = {'research_dataset': self.cr_test_data['research_dataset']} - cr['research_dataset']['preferred_identifier'] = 'doi:10.5061/dryad.10188854' - cr['data_catalog'] = 3 - cr['metadata_provider_org'] = 'metax' - cr['metadata_provider_user'] = 'metax' - cr['research_dataset'].pop('publisher', None) - - response = self.client.post('/rest/datasets', cr, format="json") + cr = {"research_dataset": self.cr_test_data["research_dataset"]} + cr["research_dataset"]["preferred_identifier"] = "doi:10.5061/dryad.10188854" + cr["data_catalog"] = 3 + cr["metadata_provider_org"] = "metax" + cr["metadata_provider_user"] = "metax" + cr["research_dataset"].pop("publisher", None) + + response = self.client.post("/rest/datasets", cr, format="json") # Publisher value is required for datacite format, so this should return Http400 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('a required value for datacite format' in response.data['detail'][0], True, response.data) + self.assertEqual( + "a required value for datacite format" in response.data["detail"][0], + True, + response.data, + ) + # # helpers # @@ -589,14 +756,15 @@ def _set_preferred_identifier_to_record(self, pk=None, catalog_id=None): Set preferred_identifier to an existing record to a value, and return that value, which will then be used by the test to create or update another record. """ - unique_identifier = 'im unique yo' + unique_identifier = "im unique yo" cr = CatalogRecord.objects.get(pk=pk) - cr.research_dataset['preferred_identifier'] = unique_identifier + cr.research_dataset["preferred_identifier"] = unique_identifier cr.data_catalog_id = catalog_id cr.force_save() cr._handle_preferred_identifier_changed() return unique_identifier + class CatalogRecordApiWriteDatasetSchemaSelection(CatalogRecordApiWriteCommon): # # @@ -613,18 +781,18 @@ def setUp(self): def test_catalog_record_with_not_found_json_schema_gets_default_schema(self): # catalog has dataset schema, but it is not found on the server dc = DataCatalog.objects.get(pk=1) - dc.catalog_json['research_dataset_schema'] = 'nonexisting' + dc.catalog_json["research_dataset_schema"] = "nonexisting" dc.save() - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # catalog has no dataset schema at all dc = DataCatalog.objects.get(pk=1) - dc.catalog_json.pop('research_dataset_schema') + dc.catalog_json.pop("research_dataset_schema") dc.save() - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def test_catalog_record_create_with_other_schema(self): @@ -632,21 +800,23 @@ def test_catalog_record_create_with_other_schema(self): Ensure that dataset json schema validation works with other json schemas than the default IDA """ - self.cr_test_data['research_dataset']['remote_resources'] = [ - {'title': 'title'}, - {'title': 'title'} + self.cr_test_data["research_dataset"]["remote_resources"] = [ + {"title": "title"}, + {"title": "title"}, ] - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.cr_test_data['research_dataset']['remote_resources'] = [ - {'title': 'title'}, - {'title': 'title'}, - {'woah': 'this should give a failure, since title is a required field, and it is missing'} + self.cr_test_data["research_dataset"]["remote_resources"] = [ + {"title": "title"}, + {"title": "title"}, + { + "woah": "this should give a failure, since title is a required field, and it is missing" + }, ] - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_catalog_record_ref_data_validation_with_other_schema(self): @@ -654,26 +824,27 @@ def test_catalog_record_ref_data_validation_with_other_schema(self): Ensure that dataset reference data validation and population works with other json schemas than the default IDA. Ref data validation should be schema agnostic """ - self.cr_test_data['research_dataset']['other_identifier'] = [ + self.cr_test_data["research_dataset"]["other_identifier"] = [ { - 'notation': 'urn:1', - 'type': { - 'identifier': 'doi', - } + "notation": "urn:1", + "type": { + "identifier": "doi", + }, } ] - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assertEqual( - 'uri.suomi.fi' in response.data['research_dataset']['other_identifier'][0]['type']['identifier'], + "uri.suomi.fi" + in response.data["research_dataset"]["other_identifier"][0]["type"]["identifier"], True, - 'Identifier type should have been populated with data from ref data' + "Identifier type should have been populated with data from ref data", ) def _set_data_catalog_schema_to_harvester(self): dc = DataCatalog.objects.get(pk=1) - dc.catalog_json['research_dataset_schema'] = 'harvester' + dc.catalog_json["research_dataset_schema"] = "harvester" dc.save() @@ -685,38 +856,50 @@ class CatalogRecordApiWriteUpdateTests(CatalogRecordApiWriteCommon): # def test_update_catalog_record(self): - cr = self.client.get('/rest/datasets/1').data - cr['preservation_description'] = 'what' + cr = self.client.get("/rest/datasets/1").data + cr["preservation_description"] = "what" - response = self.client.put('/rest/datasets/1', cr, format="json") + response = self.client.put("/rest/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['preservation_description'], 'what') + self.assertEqual(response.data["preservation_description"], "what") cr = CatalogRecord.objects.get(pk=1) - self.assertEqual(cr.date_modified >= get_tz_aware_now_without_micros() - timedelta(seconds=5), True, - 'Timestamp should have been updated during object update') + self.assertEqual( + cr.date_modified >= get_tz_aware_now_without_micros() - timedelta(seconds=5), + True, + "Timestamp should have been updated during object update", + ) def test_update_catalog_record_error_using_preferred_identifier(self): - cr = self.client.get('/rest/datasets/1').data - response = self.client.put('/rest/datasets/%s' % cr['research_dataset']['preferred_identifier'], - { 'whatever': 123 }, format="json") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, - 'Update operation should return 404 when using preferred_identifier') + cr = self.client.get("/rest/datasets/1").data + response = self.client.put( + "/rest/datasets/%s" % cr["research_dataset"]["preferred_identifier"], + {"whatever": 123}, + format="json", + ) + self.assertEqual( + response.status_code, + status.HTTP_404_NOT_FOUND, + "Update operation should return 404 when using preferred_identifier", + ) def test_update_catalog_record_error_required_fields(self): """ Field 'research_dataset' is missing, which should result in an error, since PUT replaces an object and requires all 'required' fields to be present. """ - cr = self.client.get('/rest/datasets/1').data - cr.pop('research_dataset') - response = self.client.put('/rest/datasets/1', cr, format="json") + cr = self.client.get("/rest/datasets/1").data + cr.pop("research_dataset") + response = self.client.put("/rest/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('research_dataset' in response.data.keys(), True, - 'Error for field \'research_dataset\' is missing from response.data') + self.assertEqual( + "research_dataset" in response.data.keys(), + True, + "Error for field 'research_dataset' is missing from response.data", + ) def test_update_catalog_record_not_found(self): - response = self.client.put('/rest/datasets/doesnotexist', self.cr_test_data, format="json") + response = self.client.put("/rest/datasets/doesnotexist", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_update_catalog_record_contract(self): @@ -725,29 +908,33 @@ def test_update_catalog_record_contract(self): old_contract_id = cr.contract.id # update contract to any different contract - cr_1 = self.client.get('/rest/datasets/%d' % cr.id).data - cr_1['contract'] = Contract.objects.all().exclude(pk=old_contract_id).first().id + cr_1 = self.client.get("/rest/datasets/%d" % cr.id).data + cr_1["contract"] = Contract.objects.all().exclude(pk=old_contract_id).first().id - response = self.client.put('/rest/datasets/%d' % cr.id, cr_1, format="json") + response = self.client.put("/rest/datasets/%d" % cr.id, cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) new_contract_id = CatalogRecord.objects.get(pk=cr.id).contract.id - self.assertNotEqual(old_contract_id, new_contract_id, 'Contract should have changed') + self.assertNotEqual(old_contract_id, new_contract_id, "Contract should have changed") def test_catalog_record_update_allowed_projects_ok(self): - cr_11 = self.client.get('/rest/datasets/11').data - cr_11['preservation_state'] = 0 - cr_11_dir_len = len(cr_11['research_dataset']['directories']) - cr_11['research_dataset']['directories'].pop(1) + cr_11 = self.client.get("/rest/datasets/11").data + cr_11["preservation_state"] = 0 + cr_11_dir_len = len(cr_11["research_dataset"]["directories"]) + cr_11["research_dataset"]["directories"].pop(1) - response = self.client.put('/rest/datasets/11?allowed_projects=project_x', cr_11, format="json") + response = self.client.put( + "/rest/datasets/11?allowed_projects=project_x", cr_11, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['research_dataset']['directories']), cr_11_dir_len - 1) + self.assertEqual(len(response.data["research_dataset"]["directories"]), cr_11_dir_len - 1) def test_catalog_record_update_allowed_projects_fail(self): - cr_1 = self.client.get('/rest/datasets/1').data - cr_1['research_dataset']['files'].pop(0) + cr_1 = self.client.get("/rest/datasets/1").data + cr_1["research_dataset"]["files"].pop(0) - response = self.client.put('/rest/datasets/1?allowed_projects=no,projects', cr_1, format="json") + response = self.client.put( + "/rest/datasets/1?allowed_projects=no,projects", cr_1, format="json" + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) # @@ -755,105 +942,115 @@ def test_catalog_record_update_allowed_projects_fail(self): # def test_catalog_record_update_list(self): - cr_1 = self.client.get('/rest/datasets/1').data - cr_1['preservation_description'] = 'updated description' + cr_1 = self.client.get("/rest/datasets/1").data + cr_1["preservation_description"] = "updated description" - cr_2 = self.client.get('/rest/datasets/2').data - cr_2['preservation_description'] = 'second updated description' + cr_2 = self.client.get("/rest/datasets/2").data + cr_2["preservation_description"] = "second updated description" - response = self.client.put('/rest/datasets', [ cr_1, cr_2 ], format="json") + response = self.client.put("/rest/datasets", [cr_1, cr_2], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['success']), 2) + self.assertEqual(len(response.data["success"]), 2) updated_cr = CatalogRecord.objects.get(pk=1) - self.assertEqual(updated_cr.preservation_description, 'updated description') + self.assertEqual(updated_cr.preservation_description, "updated description") updated_cr = CatalogRecord.objects.get(pk=2) - self.assertEqual(updated_cr.preservation_description, 'second updated description') + self.assertEqual(updated_cr.preservation_description, "second updated description") def test_catalog_record_update_list_error_one_fails(self): - cr_1 = self.client.get('/rest/datasets/1').data - cr_1['preservation_description'] = 'updated description' + cr_1 = self.client.get("/rest/datasets/1").data + cr_1["preservation_description"] = "updated description" # data catalog is a required field, should therefore fail - cr_2 = self.client.get('/rest/datasets/2').data - cr_2.pop('data_catalog', None) + cr_2 = self.client.get("/rest/datasets/2").data + cr_2.pop("data_catalog", None) - response = self.client.put('/rest/datasets', [ cr_1, cr_2 ], format="json") + response = self.client.put("/rest/datasets", [cr_1, cr_2], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual(isinstance(response.data['success'], list), True, - 'return data should contain key success, which is a list') - self.assertEqual(len(response.data['success']), 1) - self.assertEqual(len(response.data['failed']), 1) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual("failed" in response.data.keys(), True) + self.assertEqual( + isinstance(response.data["success"], list), + True, + "return data should contain key success, which is a list", + ) + self.assertEqual(len(response.data["success"]), 1) + self.assertEqual(len(response.data["failed"]), 1) updated_cr = CatalogRecord.objects.get(pk=1) - self.assertEqual(updated_cr.preservation_description, 'updated description') + self.assertEqual(updated_cr.preservation_description, "updated description") def test_catalog_record_update_list_error_key_not_found(self): # does not have identifier key - cr_1 = self.client.get('/rest/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1['research_dataset'].pop('metadata_version_identifier') + cr_1 = self.client.get("/rest/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1["research_dataset"].pop("metadata_version_identifier") - cr_2 = self.client.get('/rest/datasets/2').data - cr_2['preservation_description'] = 'second updated description' + cr_2 = self.client.get("/rest/datasets/2").data + cr_2["preservation_description"] = "second updated description" - response = self.client.put('/rest/datasets', [ cr_1, cr_2 ], format="json") + response = self.client.put("/rest/datasets", [cr_1, cr_2], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 1) - self.assertEqual(len(response.data['failed']), 1) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual("failed" in response.data.keys(), True) + self.assertEqual(len(response.data["success"]), 1) + self.assertEqual(len(response.data["failed"]), 1) def test_catalog_record_deprecated_and_date_deprecated_cannot_be_set(self): # Test catalog record's deprecated field cannot be set with POST, PUT or PATCH initial_deprecated = True - self.cr_test_data['deprecated'] = initial_deprecated - self.cr_test_data['date_deprecated'] = '2018-01-01T00:00:00' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") - self.assertEqual(response.data['deprecated'], False) - self.assertTrue('date_deprecated' not in response.data) - - response_json = self.client.get('/rest/datasets/1').data - initial_deprecated = response_json['deprecated'] - response_json['deprecated'] = not initial_deprecated - response_json['date_deprecated'] = '2018-01-01T00:00:00' - response = self.client.put('/rest/datasets/1', response_json, format="json") + self.cr_test_data["deprecated"] = initial_deprecated + self.cr_test_data["date_deprecated"] = "2018-01-01T00:00:00" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") + self.assertEqual(response.data["deprecated"], False) + self.assertTrue("date_deprecated" not in response.data) + + response_json = self.client.get("/rest/datasets/1").data + initial_deprecated = response_json["deprecated"] + response_json["deprecated"] = not initial_deprecated + response_json["date_deprecated"] = "2018-01-01T00:00:00" + response = self.client.put("/rest/datasets/1", response_json, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['deprecated'], initial_deprecated) - self.assertTrue('date_deprecated' not in response.data) + self.assertEqual(response.data["deprecated"], initial_deprecated) + self.assertTrue("date_deprecated" not in response.data) - initial_deprecated = self.client.get('/rest/datasets/1').data['deprecated'] - response = self.client.patch('/rest/datasets/1', { 'deprecated': not initial_deprecated }, format="json") + initial_deprecated = self.client.get("/rest/datasets/1").data["deprecated"] + response = self.client.patch( + "/rest/datasets/1", {"deprecated": not initial_deprecated}, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['deprecated'], initial_deprecated) - self.assertTrue('date_deprecated' not in response.data) + self.assertEqual(response.data["deprecated"], initial_deprecated) + self.assertTrue("date_deprecated" not in response.data) def test_catalog_record_date_deprecated_and_date_deprecated_lifecycle(self): # if dataset is deprecated, fixing dataset creates new version ds = CatalogRecord.objects.filter(files__id=1) ds_id = ds[0].identifier - response = self.client.delete('/rest/files/1') + response = self.client.delete("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/datasets/%s' % ds_id) + response = self.client.get("/rest/datasets/%s" % ds_id) cr = response.data - self.assertTrue(cr['deprecated']) - self.assertTrue(cr['date_deprecated'].startswith('2')) + self.assertTrue(cr["deprecated"]) + self.assertTrue(cr["date_deprecated"].startswith("2")) - response = self.client.post('/rpc/datasets/fix_deprecated?identifier=%s' % ds_id, cr, format="json") + response = self.client.post( + "/rpc/datasets/fix_deprecated?identifier=%s" % ds_id, cr, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(CatalogRecord.objects.get(identifier=ds_id).next_dataset_version.deprecated, False) + self.assertEqual( + CatalogRecord.objects.get(identifier=ds_id).next_dataset_version.deprecated, + False, + ) def test_catalog_record_deprecation_updates_date_modified(self): cr = CatalogRecord.objects.filter(files__id=1) cr_id = cr[0].identifier - response = self.client.delete('/rest/files/1') + response = self.client.delete("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr_depr = CatalogRecord.objects.get(identifier=cr_id) @@ -864,41 +1061,41 @@ def test_change_datacatalog_ATT_to_IDA(self): cr = self._get_new_full_test_att_cr_data() # create ATT data catalog - dc_att = self._get_object_from_test_data('datacatalog', 4) - dc_att['catalog_json']['identifier'] = 'urn:nbn:fi:att:data-catalog-att' - dc_att = self.client.post('/rest/datacatalogs', dc_att, format="json").data + dc_att = self._get_object_from_test_data("datacatalog", 4) + dc_att["catalog_json"]["identifier"] = "urn:nbn:fi:att:data-catalog-att" + dc_att = self.client.post("/rest/datacatalogs", dc_att, format="json").data # create IDA data catalog - dc_ida = self._get_object_from_test_data('datacatalog') - dc_ida['catalog_json']['identifier'] = 'urn:nbn:fi:att:data-catalog-ida' - dc_ida = self.client.post('/rest/datacatalogs', dc_ida, format="json").data + dc_ida = self._get_object_from_test_data("datacatalog") + dc_ida["catalog_json"]["identifier"] = "urn:nbn:fi:att:data-catalog-ida" + dc_ida = self.client.post("/rest/datacatalogs", dc_ida, format="json").data # create ATT catalog record - cr['data_catalog'] = dc_att - cr_att = self.client.post('/rest/datasets', cr, format="json").data + cr["data_catalog"] = dc_att + cr_att = self.client.post("/rest/datasets", cr, format="json").data # change data catalog to IDA - cr_id = cr_att['id'] - cr_att['data_catalog']['id'] = dc_ida['id'] - cr_att['data_catalog']['identifier'] = dc_ida['catalog_json']['identifier'] - cr_ida = self.client.put('/rest/datasets/%d' % cr_id, cr_att, format="json") + cr_id = cr_att["id"] + cr_att["data_catalog"]["id"] = dc_ida["id"] + cr_att["data_catalog"]["identifier"] = dc_ida["catalog_json"]["identifier"] + cr_ida = self.client.put("/rest/datasets/%d" % cr_id, cr_att, format="json") self.assertEqual(cr_ida.status_code, status.HTTP_200_OK, cr_ida) - self.assertTrue(not all(item in cr_ida.data['research_dataset'].keys() for item in - ['remote_resources', 'total_remote_resources_byte_size'])) - self.assertTrue('metadata_version_identifier' in cr_ida.data['research_dataset'].keys()) + self.assertTrue( + not all( + item in cr_ida.data["research_dataset"].keys() + for item in ["remote_resources", "total_remote_resources_byte_size"] + ) + ) + self.assertTrue("metadata_version_identifier" in cr_ida.data["research_dataset"].keys()) - cr_ida.data['research_dataset']['files'] = [ + cr_ida.data["research_dataset"]["files"] = [ { "title": "File metadata title 1", "file_type": { "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text", - "pref_label": { - "en": "Text", - "fi": "Teksti", - "und": "Teksti" - } + "pref_label": {"en": "Text", "fi": "Teksti", "und": "Teksti"}, }, "identifier": "pid:urn:1", "use_category": { @@ -907,14 +1104,18 @@ def test_change_datacatalog_ATT_to_IDA(self): "pref_label": { "en": "Source material", "fi": "Lähdeaineisto", - "und": "Lähdeaineisto" - } - } - }] - cr_ida = self.client.put('/rest/datasets/%d' % cr_id, cr_ida.data, format="json") + "und": "Lähdeaineisto", + }, + }, + } + ] + cr_ida = self.client.put("/rest/datasets/%d" % cr_id, cr_ida.data, format="json") self.assertEqual(cr_ida.status_code, status.HTTP_200_OK, cr_ida.data) - self.assertTrue(len(cr_ida.data['research_dataset']['files']) == 1, 'Dataset must contain one file') + self.assertTrue( + len(cr_ida.data["research_dataset"]["files"]) == 1, + "Dataset must contain one file", + ) class CatalogRecordApiWritePartialUpdateTests(CatalogRecordApiWriteCommon): @@ -925,15 +1126,23 @@ class CatalogRecordApiWritePartialUpdateTests(CatalogRecordApiWriteCommon): # def test_update_catalog_record_partial(self): - new_data_catalog = self._get_object_from_test_data('datacatalog', requested_index=1)['id'] + new_data_catalog = self._get_object_from_test_data("datacatalog", requested_index=1)["id"] new_data = { "data_catalog": new_data_catalog, } - response = self.client.patch('/rest/datasets/%s' % self.identifier, new_data, format="json") + response = self.client.patch("/rest/datasets/%s" % self.identifier, new_data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('research_dataset' in response.data.keys(), True, 'PATCH operation should return full content') - self.assertEqual(response.data['data_catalog']['id'], new_data_catalog, 'Field data_catalog was not updated') + self.assertEqual( + "research_dataset" in response.data.keys(), + True, + "PATCH operation should return full content", + ) + self.assertEqual( + response.data["data_catalog"]["id"], + new_data_catalog, + "Field data_catalog was not updated", + ) # # update list operations PATCH @@ -941,59 +1150,80 @@ def test_update_catalog_record_partial(self): def test_catalog_record_partial_update_list(self): test_data = {} - test_data['id'] = 1 - test_data['preservation_description'] = 'description' + test_data["id"] = 1 + test_data["preservation_description"] = "description" second_test_data = {} - second_test_data['id'] = 2 - second_test_data['preservation_description'] = 'description 2' + second_test_data["id"] = 2 + second_test_data["preservation_description"] = "description 2" - response = self.client.patch('/rest/datasets', [test_data, second_test_data], format="json") + response = self.client.patch("/rest/datasets", [test_data, second_test_data], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data, True, 'response.data should contain list of changed objects') - self.assertEqual(len(response.data), 2, 'response.data should contain 2 changed objects') - self.assertEqual('research_dataset' in response.data['success'][0]['object'], True, - 'response.data should contain full objects') + self.assertEqual( + "success" in response.data, + True, + "response.data should contain list of changed objects", + ) + self.assertEqual(len(response.data), 2, "response.data should contain 2 changed objects") + self.assertEqual( + "research_dataset" in response.data["success"][0]["object"], + True, + "response.data should contain full objects", + ) updated_cr = CatalogRecord.objects.get(pk=1) - self.assertEqual(updated_cr.preservation_description, 'description') + self.assertEqual(updated_cr.preservation_description, "description") def test_catalog_record_partial_update_list_error_one_fails(self): test_data = {} - test_data['id'] = 1 - test_data['preservation_description'] = 'description' + test_data["id"] = 1 + test_data["preservation_description"] = "description" second_test_data = {} - second_test_data['preservation_state'] = 555 # value not allowed - second_test_data['id'] = 2 + second_test_data["preservation_state"] = 555 # value not allowed + second_test_data["id"] = 2 - response = self.client.patch('/rest/datasets', [test_data, second_test_data], format="json") + response = self.client.patch("/rest/datasets", [test_data, second_test_data], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 1, 'success list should contain one item') - self.assertEqual(len(response.data['failed']), 1, 'there should have been one failed element') - self.assertEqual('preservation_state' in response.data['failed'][0]['errors'], True, - response.data['failed'][0]['errors']) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual("failed" in response.data.keys(), True) + self.assertEqual(len(response.data["success"]), 1, "success list should contain one item") + self.assertEqual( + len(response.data["failed"]), 1, "there should have been one failed element" + ) + self.assertEqual( + "preservation_state" in response.data["failed"][0]["errors"], + True, + response.data["failed"][0]["errors"], + ) def test_catalog_record_partial_update_list_error_key_not_found(self): # does not have identifier key test_data = {} - test_data['preservation_state'] = 10 + test_data["preservation_state"] = 10 second_test_data = {} - second_test_data['id'] = 2 - second_test_data['preservation_state'] = 20 + second_test_data["id"] = 2 + second_test_data["preservation_state"] = 20 - response = self.client.patch('/rest/datasets', [test_data, second_test_data], format="json") + response = self.client.patch("/rest/datasets", [test_data, second_test_data], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 1, 'success list should contain one item') - self.assertEqual(len(response.data['failed']), 1, 'there should have been one failed element') - self.assertEqual('detail' in response.data['failed'][0]['errors'], True, response.data['failed'][0]['errors']) - self.assertEqual('identifying key' in response.data['failed'][0]['errors']['detail'][0], True, - response.data['failed'][0]['errors']) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual("failed" in response.data.keys(), True) + self.assertEqual(len(response.data["success"]), 1, "success list should contain one item") + self.assertEqual( + len(response.data["failed"]), 1, "there should have been one failed element" + ) + self.assertEqual( + "detail" in response.data["failed"][0]["errors"], + True, + response.data["failed"][0]["errors"], + ) + self.assertEqual( + "identifying key" in response.data["failed"][0]["errors"]["detail"][0], + True, + response.data["failed"][0]["errors"], + ) class CatalogRecordApiWriteDeleteTests(CatalogRecordApiWriteCommon): @@ -1006,7 +1236,7 @@ class CatalogRecordApiWriteDeleteTests(CatalogRecordApiWriteCommon): # def test_delete_catalog_record(self): - url = '/rest/datasets/%s' % self.identifier + url = "/rest/datasets/%s" % self.identifier response = self.client.delete(url) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) response = self.client.get(url) @@ -1014,76 +1244,101 @@ def test_delete_catalog_record(self): try: deleted_catalog_record = CatalogRecord.objects.get(identifier=self.identifier) - raise Exception('Deleted CatalogRecord should not be retrievable from the default objects table') + raise Exception( + "Deleted CatalogRecord should not be retrievable from the default objects table" + ) except CatalogRecord.DoesNotExist: # successful test should go here, instead of raising the expection in try: block pass try: - deleted_catalog_record = CatalogRecord.objects_unfiltered.get(identifier=self.identifier) + deleted_catalog_record = CatalogRecord.objects_unfiltered.get( + identifier=self.identifier + ) except CatalogRecord.DoesNotExist: - raise Exception('Deleted CatalogRecord should not be deleted from the db, but marked as removed') + raise Exception( + "Deleted CatalogRecord should not be deleted from the db, but marked as removed" + ) self.assertEqual(deleted_catalog_record.removed, True) self.assertEqual(deleted_catalog_record.identifier, self.identifier) - self.assertEqual(deleted_catalog_record.date_modified, deleted_catalog_record.date_removed, - 'date_modified should be updated') + self.assertEqual( + deleted_catalog_record.date_modified, + deleted_catalog_record.date_removed, + "date_modified should be updated", + ) def test_delete_catalog_record_error_using_preferred_identifier(self): - url = '/rest/datasets/%s' % self.preferred_identifier + url = "/rest/datasets/%s" % self.preferred_identifier response = self.client.delete(url) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_bulk_delete_catalog_record_permissions(self): # create catalog with 'metax' edit permissions and create dataset with this catalog as 'metax' user cr = self._get_new_test_cr_data() - cr.pop('id') - catalog = self._get_object_from_test_data('datacatalog', requested_index=0) - catalog.pop('id') - catalog['catalog_json']['identifier'] = 'metax-catalog' - catalog['catalog_record_services_edit'] = 'metax' - catalog = self.client.post('/rest/datacatalogs', catalog, format="json") - cr['data_catalog'] = {'id': catalog.data['id'], 'identifier': catalog.data['catalog_json']['identifier']} - - self._use_http_authorization(username='metax') - response = self.client.post('/rest/datasets/', cr, format="json") - metax_cr = response.data['id'] + cr.pop("id") + catalog = self._get_object_from_test_data("datacatalog", requested_index=0) + catalog.pop("id") + catalog["catalog_json"]["identifier"] = "metax-catalog" + catalog["catalog_record_services_edit"] = "metax" + catalog = self.client.post("/rest/datacatalogs", catalog, format="json") + cr["data_catalog"] = { + "id": catalog.data["id"], + "identifier": catalog.data["catalog_json"]["identifier"], + } + + self._use_http_authorization(username="metax") + response = self.client.post("/rest/datasets/", cr, format="json") + metax_cr = response.data["id"] # create catalog with 'testuser' edit permissions and create dataset with this catalog as 'testuser' user cr = self._get_new_test_cr_data() - cr.pop('id') - catalog = self._get_object_from_test_data('datacatalog', requested_index=1) - catalog.pop('id') - catalog['catalog_json']['identifier'] = 'testuser-catalog' - catalog['catalog_record_services_edit'] = 'testuser' - catalog = self.client.post('/rest/datacatalogs', catalog, format="json") - cr['data_catalog'] = {'id': catalog.data['id'], 'identifier': catalog.data['catalog_json']['identifier']} - - self._use_http_authorization(username='testuser', password='testuserpassword') - response = self.client.post('/rest/datasets/', cr, format="json") - testuser_cr = response.data['id'] + cr.pop("id") + catalog = self._get_object_from_test_data("datacatalog", requested_index=1) + catalog.pop("id") + catalog["catalog_json"]["identifier"] = "testuser-catalog" + catalog["catalog_record_services_edit"] = "testuser" + catalog = self.client.post("/rest/datacatalogs", catalog, format="json") + cr["data_catalog"] = { + "id": catalog.data["id"], + "identifier": catalog.data["catalog_json"]["identifier"], + } + + self._use_http_authorization(username="testuser", password="testuserpassword") + response = self.client.post("/rest/datasets/", cr, format="json") + testuser_cr = response.data["id"] # after trying to delete as 'testuser' only one catalog is deleted - response = self.client.delete('/rest/datasets', [metax_cr, testuser_cr], format="json") + response = self.client.delete("/rest/datasets", [metax_cr, testuser_cr], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, [testuser_cr]) - response = self.client.post('/rest/datasets/list?pagination=false', [metax_cr, testuser_cr], format="json") + response = self.client.post( + "/rest/datasets/list?pagination=false", + [metax_cr, testuser_cr], + format="json", + ) self.assertTrue(len(response.data), 1) - response = self.client.delete('/rest/datasets', [metax_cr], format="json") + response = self.client.delete("/rest/datasets", [metax_cr], format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.post('/rest/datasets/list?pagination=false', [metax_cr, testuser_cr], format="json") + response = self.client.post( + "/rest/datasets/list?pagination=false", + [metax_cr, testuser_cr], + format="json", + ) self.assertTrue(len(response.data), 1) def test_bulk_delete_catalog_record(self): ids = [1, 2, 3] - identifiers = CatalogRecord.objects.filter(pk__in=[4, 5, 6]).values_list('identifier', flat=True) + identifiers = CatalogRecord.objects.filter(pk__in=[4, 5, 6]).values_list( + "identifier", flat=True + ) for crs in [ids, identifiers]: - response = self.client.delete('/rest/datasets', crs, format="json") + response = self.client.delete("/rest/datasets", crs, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(response.data == [1, 2, 3] or response.data == [4, 5, 6]) - response = self.client.post('/rest/datasets/list?pagination=false', crs, format="json") + response = self.client.post("/rest/datasets/list?pagination=false", crs, format="json") self.assertFalse(response.data) for cr in crs: @@ -1093,21 +1348,24 @@ def test_bulk_delete_catalog_record(self): deleted = CatalogRecord.objects_unfiltered.get(identifier=cr) self.assertEqual(deleted.removed, True) - self.assertEqual(deleted.date_modified, deleted.date_removed, - 'date_modified should be updated') + self.assertEqual( + deleted.date_modified, + deleted.date_removed, + "date_modified should be updated", + ) # failing tests ids = [1000, 2000] - identifiers = ['1000', '2000'] + identifiers = ["1000", "2000"] for crs in [ids, identifiers]: - response = self.client.delete('/rest/datasets', ids, format="json") + response = self.client.delete("/rest/datasets", ids, format="json") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) ids = [] - response = self.client.delete('/rest/datasets', ids, format="json") + response = self.client.delete("/rest/datasets", ids, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('Received empty list of identifiers' in response.data['detail'][0]) + self.assertTrue("Received empty list of identifiers" in response.data["detail"][0]) class CatalogRecordApiWritePreservationStateTests(CatalogRecordApiWriteCommon): @@ -1121,12 +1379,12 @@ def _create_pas_dataset_from_id(self, id): Helper method to create a pas dataset by updating the given dataset's preservation_state to 80. """ - cr_data = self.client.get('/rest/datasets/%d' % id, format="json").data - self.assertEqual(cr_data['preservation_state'], 0) + cr_data = self.client.get("/rest/datasets/%d" % id, format="json").data + self.assertEqual(cr_data["preservation_state"], 0) # update state to "accepted to pas" -> should create pas version - cr_data['preservation_state'] = 80 - response = self.client.put('/rest/datasets/%d' % id, cr_data, format="json") + cr_data["preservation_state"] = 80 + response = self.client.put("/rest/datasets/%d" % id, cr_data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) return response.data @@ -1134,25 +1392,29 @@ def setUp(self): super().setUp() dc = DataCatalog.objects.get(pk=1) catalog_json = dc.catalog_json - catalog_json['identifier'] = django_settings.PAS_DATA_CATALOG_IDENTIFIER - catalog_json['dataset_versioning'] = False + catalog_json["identifier"] = django_settings.PAS_DATA_CATALOG_IDENTIFIER + catalog_json["dataset_versioning"] = False dc = DataCatalog.objects.create( catalog_json=catalog_json, date_created=get_tz_aware_now_without_micros(), - catalog_record_services_create='testuser,api_auth_user,metax', - catalog_record_services_edit='testuser,api_auth_user,metax', - catalog_record_services_read='testuser,api_auth_user,metax' + catalog_record_services_create="testuser,api_auth_user,metax", + catalog_record_services_edit="testuser,api_auth_user,metax", + catalog_record_services_read="testuser,api_auth_user,metax", ) def test_update_catalog_record_pas_state_allowed_value(self): - cr = self.client.get('/rest/datasets/1').data - cr['preservation_state'] = 30 - response = self.client.put('/rest/datasets/1', cr, format="json") + cr = self.client.get("/rest/datasets/1").data + cr["preservation_state"] = 30 + response = self.client.put("/rest/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) cr = CatalogRecord.objects.get(pk=1) - self.assertEqual(cr.preservation_state_modified >= get_tz_aware_now_without_micros() - timedelta(seconds=5), - True, 'Timestamp should have been updated during object update') + self.assertEqual( + cr.preservation_state_modified + >= get_tz_aware_now_without_micros() - timedelta(seconds=5), + True, + "Timestamp should have been updated during object update", + ) def test_update_pas_state_to_needs_revalidation(self): """ @@ -1167,14 +1429,14 @@ def test_update_pas_state_to_needs_revalidation(self): cr.save() # retrieve record and ensure testing state was set correctly... - cr_data = self.client.get('/rest/datasets/1', format="json").data - self.assertEqual(cr_data['preservation_state'], preservation_state_value) + cr_data = self.client.get("/rest/datasets/1", format="json").data + self.assertEqual(cr_data["preservation_state"], preservation_state_value) # strike and verify - cr_data['research_dataset']['title']['en'] = 'Metadata has been updated on loop %d' % i - response = self.client.put('/rest/datasets/1', cr_data, format="json") + cr_data["research_dataset"]["title"]["en"] = "Metadata has been updated on loop %d" % i + response = self.client.put("/rest/datasets/1", cr_data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['preservation_state'], 60) + self.assertEqual(response.data["preservation_state"], 60) def test_prevent_file_changes_when_record_in_pas_process(self): """ @@ -1183,42 +1445,44 @@ def test_prevent_file_changes_when_record_in_pas_process(self): cr = CatalogRecord.objects.get(pk=1) cr.preservation_state = 10 cr.save() - cr_data = self.client.get('/rest/datasets/1', format="json").data - cr_data['research_dataset']['files'].pop(0) - response = self.client.put('/rest/datasets/1', cr_data, format="json") + cr_data = self.client.get("/rest/datasets/1", format="json").data + cr_data["research_dataset"]["files"].pop(0) + response = self.client.put("/rest/datasets/1", cr_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('PAS process' in response.data['detail'][0], True, response.data) + self.assertEqual("PAS process" in response.data["detail"][0], True, response.data) def test_non_pas_dataset_unallowed_preservation_state_values(self): # update non-pas dataset - cr = self.client.get('/rest/datasets/1').data + cr = self.client.get("/rest/datasets/1").data values = [ - 11, # not one of known values - 90, # value not allowed for non-pas datasets + 11, # not one of known values + 90, # value not allowed for non-pas datasets ] for invalid_value in values: - cr['preservation_state'] = invalid_value - response = self.client.put('/rest/datasets/1', cr, format="json") + cr["preservation_state"] = invalid_value + response = self.client.put("/rest/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_pas_dataset_unallowed_preservation_state_values(self): # create pas dataset and update with invalid values - cr = self.client.get('/rest/datasets/1').data - cr['preservation_state'] = 80 - response = self.client.put('/rest/datasets/1', cr, format="json") - cr = self.client.get('/rest/datasets/%d' % response.data['preservation_dataset_version']['id']).data + cr = self.client.get("/rest/datasets/1").data + cr["preservation_state"] = 80 + response = self.client.put("/rest/datasets/1", cr, format="json") + cr = self.client.get( + "/rest/datasets/%d" % response.data["preservation_dataset_version"]["id"] + ).data values = [ 70, # value not allowed for non-pas datasets - 111, # not one of known values - 150 # not one of known values + 111, # not one of known values + 150, # not one of known values ] for invalid_value in values: - cr['preservation_state'] = invalid_value - response = self.client.put('/rest/datasets/1', cr, format="json") + cr["preservation_state"] = invalid_value + response = self.client.put("/rest/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_pas_version_is_created_on_preservation_state_80(self): @@ -1226,35 +1490,48 @@ def test_pas_version_is_created_on_preservation_state_80(self): When preservation_state is updated to 'accepted to pas', a copy should be created into designated PAS catalog. """ - cr_data = self.client.get('/rest/datasets/1', format="json").data - self.assertEqual(cr_data['preservation_state'], 0) + cr_data = self.client.get("/rest/datasets/1", format="json").data + self.assertEqual(cr_data["preservation_state"], 0) origin_dataset = self._create_pas_dataset_from_id(1) - self.assertEqual(origin_dataset['preservation_state'], 0) - self.assertEqual('new_version_created' in origin_dataset, True) - self.assertEqual(origin_dataset['new_version_created']['version_type'], 'pas') - self.assertEqual('preservation_dataset_version' in origin_dataset, True) - self.assertEqual('other_identifier' in origin_dataset['research_dataset'], True) - self.assertEqual(origin_dataset['research_dataset']['other_identifier'][0]['notation'].startswith('doi'), True) + self.assertEqual(origin_dataset["preservation_state"], 0) + self.assertEqual("new_version_created" in origin_dataset, True) + self.assertEqual(origin_dataset["new_version_created"]["version_type"], "pas") + self.assertEqual("preservation_dataset_version" in origin_dataset, True) + self.assertEqual("other_identifier" in origin_dataset["research_dataset"], True) + self.assertEqual( + origin_dataset["research_dataset"]["other_identifier"][0]["notation"].startswith("doi"), + True, + ) # get pas version and verify links and other signature values are there pas_dataset = self.client.get( - '/rest/datasets/%d' % origin_dataset['preservation_dataset_version']['id'], format="json" + "/rest/datasets/%d" % origin_dataset["preservation_dataset_version"]["id"], + format="json", ).data - self.assertEqual(pas_dataset['data_catalog']['identifier'], django_settings.PAS_DATA_CATALOG_IDENTIFIER) - self.assertEqual(pas_dataset['preservation_state'], 80) - self.assertEqual(pas_dataset['preservation_dataset_origin_version']['id'], origin_dataset['id']) self.assertEqual( - pas_dataset['preservation_dataset_origin_version']['preferred_identifier'], - origin_dataset['research_dataset']['preferred_identifier'] + pas_dataset["data_catalog"]["identifier"], + django_settings.PAS_DATA_CATALOG_IDENTIFIER, + ) + self.assertEqual(pas_dataset["preservation_state"], 80) + self.assertEqual( + pas_dataset["preservation_dataset_origin_version"]["id"], + origin_dataset["id"], + ) + self.assertEqual( + pas_dataset["preservation_dataset_origin_version"]["preferred_identifier"], + origin_dataset["research_dataset"]["preferred_identifier"], + ) + self.assertEqual("deprecated" in pas_dataset["preservation_dataset_origin_version"], True) + self.assertEqual("other_identifier" in pas_dataset["research_dataset"], True) + self.assertEqual( + pas_dataset["research_dataset"]["other_identifier"][0]["notation"].startswith("urn"), + True, ) - self.assertEqual('deprecated' in pas_dataset['preservation_dataset_origin_version'], True) - self.assertEqual('other_identifier' in pas_dataset['research_dataset'], True) - self.assertEqual(pas_dataset['research_dataset']['other_identifier'][0]['notation'].startswith('urn'), True) # when pas copy is created, origin_dataset preservation_state should have been set back to 0 - cr_data = self.client.get('/rest/datasets/1', format="json").data - self.assertEqual(cr_data['preservation_state'], 0) + cr_data = self.client.get("/rest/datasets/1", format="json").data + self.assertEqual(cr_data["preservation_state"], 0) def test_origin_dataset_cant_have_multiple_pas_versions(self): """ @@ -1263,35 +1540,43 @@ def test_origin_dataset_cant_have_multiple_pas_versions(self): """ self._create_pas_dataset_from_id(1) - cr_data = { 'preservation_state': 80 } - response = self.client.patch('/rest/datasets/1', cr_data, format="json") + cr_data = {"preservation_state": 80} + response = self.client.patch("/rest/datasets/1", cr_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('already has a PAS version' in response.data['detail'][0], True, response.data) + self.assertEqual( + "already has a PAS version" in response.data["detail"][0], + True, + response.data, + ) def test_dataset_can_be_created_directly_into_pas_catalog(self): """ Datasets that are created directly into PAS catalog should not have any enforced rules about changing preservation_state value. """ - self.cr_test_data['data_catalog'] = django_settings.PAS_DATA_CATALOG_IDENTIFIER - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["data_catalog"] = django_settings.PAS_DATA_CATALOG_IDENTIFIER + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assertEqual( - response.data['research_dataset']['preferred_identifier'].startswith('doi'), + response.data["research_dataset"]["preferred_identifier"].startswith("doi"), True, - response.data['research_dataset']['preferred_identifier'] + response.data["research_dataset"]["preferred_identifier"], ) # when created directly into pas catalog, preservation_state can be updated # to whatever, whenever - ps_values = [ v[0] for v in CatalogRecord.PRESERVATION_STATE_CHOICES ] + ps_values = [v[0] for v in CatalogRecord.PRESERVATION_STATE_CHOICES] for ps in ps_values: - cr_data = { 'preservation_state': ps } - response = self.client.patch('/rest/datasets/%d' % response.data['id'], cr_data, format="json") + cr_data = {"preservation_state": ps} + response = self.client.patch( + "/rest/datasets/%d" % response.data["id"], cr_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - cr_data = { 'preservation_state': 0 } - response = self.client.patch('/rest/datasets/%d' % response.data['id'], cr_data, format="json") + cr_data = {"preservation_state": 0} + response = self.client.patch( + "/rest/datasets/%d" % response.data["id"], cr_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) def test_dataset_files_can_not_be_changed_in_pas_catalog(self): @@ -1302,44 +1587,53 @@ def test_dataset_files_can_not_be_changed_in_pas_catalog(self): cr = self._create_pas_dataset_from_id(1) pas_dataset = self.client.get( - '/rest/datasets/%d' % cr['preservation_dataset_version']['id'], format="json" + "/rest/datasets/%d" % cr["preservation_dataset_version"]["id"], + format="json", ).data - pas_dataset['research_dataset']['files'].pop(0) + pas_dataset["research_dataset"]["files"].pop(0) - response = self.client.put('/rest/datasets/%d' % pas_dataset['id'], pas_dataset, format="json") + response = self.client.put( + "/rest/datasets/%d" % pas_dataset["id"], pas_dataset, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('Cannot change files in' in response.data['detail'][0], True) + self.assertEqual("Cannot change files in" in response.data["detail"][0], True) def test_pas_dataset_files_equal_origin_dataset(self): """ Ensure set of files in original and pas datasets match exactly, even if more files have been frozen in between. """ - test_file = self._get_object_from_test_data('file', requested_index=0) + test_file = self._get_object_from_test_data("file", requested_index=0) response = self.client.get( - '/rest/directories/files?project=%s&path=/' % test_file['project_identifier'], format="json") + "/rest/directories/files?project=%s&path=/" % test_file["project_identifier"], + format="json", + ) - dir_identifier = response.data['directories'][0]['identifier'] + dir_identifier = response.data["directories"][0]["identifier"] # create dataset where directory along with all of its files are included - cr_data = self.client.get('/rest/datasets/1', format="json").data - cr_data['research_dataset']['directories'] = [{ - 'identifier': dir_identifier, - 'use_category': { 'identifier': 'documentation' } - }] + cr_data = self.client.get("/rest/datasets/1", format="json").data + cr_data["research_dataset"]["directories"] = [ + { + "identifier": dir_identifier, + "use_category": {"identifier": "documentation"}, + } + ] - response = self.client.put('/rest/datasets/1', cr_data, format="json") + response = self.client.put("/rest/datasets/1", cr_data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - cr_id = response.data['next_dataset_version']['id'] + cr_id = response.data["next_dataset_version"]["id"] # now freeze more files into same directory - test_file.update({ - 'file_name': '%s_new' % test_file['file_name'], - 'file_path': '%s_new' % test_file['file_path'], - 'identifier': '%s_new' % test_file['identifier'], - }) - response = self.client.post('/rest/files', test_file, format="json") + test_file.update( + { + "file_name": "%s_new" % test_file["file_name"], + "file_path": "%s_new" % test_file["file_path"], + "identifier": "%s_new" % test_file["identifier"], + } + ) + response = self.client.post("/rest/files", test_file, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # more files have been frozen in the directory, but pas dataset should not have the new frozen file, @@ -1347,8 +1641,12 @@ def test_pas_dataset_files_equal_origin_dataset(self): self._create_pas_dataset_from_id(cr_id) cr = CatalogRecord.objects.get(pk=cr_id) - cr_files = cr.files.filter().order_by('id').values_list('id', flat=True) - cr_pas_files = cr.preservation_dataset_version.files.filter().order_by('id').values_list('id', flat=True) + cr_files = cr.files.filter().order_by("id").values_list("id", flat=True) + cr_pas_files = ( + cr.preservation_dataset_version.files.filter() + .order_by("id") + .values_list("id", flat=True) + ) # note: trying to assert querysets will result in failure. must evaluate the querysets first by iterating them self.assertEqual([f for f in cr_files], [f for f in cr_pas_files]) @@ -1360,12 +1658,15 @@ def test_unfreezing_files_does_not_deprecate_pas_dataset(self): been stored in PAS. """ cr = self._create_pas_dataset_from_id(1) - response = self.client.delete('/rest/files/1', format="json") + response = self.client.delete("/rest/files/1", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/datasets/%d' % cr['preservation_dataset_version']['id'], format="json") + response = self.client.get( + "/rest/datasets/%d" % cr["preservation_dataset_version"]["id"], + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['deprecated'], False) + self.assertEqual(response.data["deprecated"], False) class CatalogRecordApiWriteReferenceDataTests(CatalogRecordApiWriteCommon): @@ -1386,26 +1687,28 @@ def test_organization_name_is_required(self): # simple case cr = deepcopy(self.cr_full_ida_test_data) - cr['research_dataset']['curator'] = [{ - '@type': 'Organization', - 'identifier': 'not found!', - # no name! - }] - response = self.client.post('/rest/datasets', cr, format="json") + cr["research_dataset"]["curator"] = [ + { + "@type": "Organization", + "identifier": "not found!", + # no name! + } + ] + response = self.client.post("/rest/datasets", cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # a more complex case. ensure organizations are found from deep structures cr = deepcopy(self.cr_full_ida_test_data) - org = cr['research_dataset']['provenance'][0]['was_associated_with'][0] - del org['name'] # should cause the error - org['@type'] = 'Organization' - org['identifier'] = 'not found!' - response = self.client.post('/rest/datasets', cr, format="json") + org = cr["research_dataset"]["provenance"][0]["was_associated_with"][0] + del org["name"] # should cause the error + org["@type"] = "Organization" + org["identifier"] = "not found!" + response = self.client.post("/rest/datasets", cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # try again. should be ok - org['identifier'] = 'http://uri.suomi.fi/codelist/fairdata/organization/code/10076' - response = self.client.post('/rest/datasets', cr, format="json") + org["identifier"] = "http://uri.suomi.fi/codelist/fairdata/organization/code/10076" + response = self.client.post("/rest/datasets", cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def test_catalog_record_reference_data_missing_ok(self): @@ -1414,11 +1717,14 @@ def test_catalog_record_reference_data_missing_ok(self): cache for whatever reason, and successfully finish the request """ cache = RedisClient() - cache.delete('reference_data') - self.assertEqual(cache.get('reference_data', master=True), None, - 'cache ref data should be missing after cache.delete()') + cache.delete("reference_data") + self.assertEqual( + cache.get("reference_data", master=True), + None, + "cache ref data should be missing after cache.delete()", + ) - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def test_missing_license_identifier_ok(self): @@ -1426,66 +1732,80 @@ def test_missing_license_identifier_ok(self): Missing license identifier is ok if url is provided. Works on att and ida datasets """ - rd_ida = self.cr_full_ida_test_data['research_dataset'] - rd_ida['access_rights']['license'] = [{ - 'license': "http://a.very.nice.custom/url" - }] - response = self.client.post('/rest/datasets', self.cr_full_ida_test_data, format="json") + rd_ida = self.cr_full_ida_test_data["research_dataset"] + rd_ida["access_rights"]["license"] = [{"license": "http://a.very.nice.custom/url"}] + response = self.client.post("/rest/datasets", self.cr_full_ida_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(len(response.data['research_dataset']['access_rights']['license'][0]), 1, response.data) + self.assertEqual( + len(response.data["research_dataset"]["access_rights"]["license"][0]), + 1, + response.data, + ) - rd_att = self.cr_full_att_test_data['research_dataset'] - rd_att['access_rights']['license'] = [{ - 'license': "http://also.fine.custom/uri", - 'description': { - 'en': "This is very informative description of this custom license." + rd_att = self.cr_full_att_test_data["research_dataset"] + rd_att["access_rights"]["license"] = [ + { + "license": "http://also.fine.custom/uri", + "description": { + "en": "This is very informative description of this custom license." + }, } - }] - rd_att['remote_resources'][0]['license'] = [{ - 'license': "http://cool.remote.uri", - 'description': { - 'en': "Proof that also remote licenses can be used with custom urls." + ] + rd_att["remote_resources"][0]["license"] = [ + { + "license": "http://cool.remote.uri", + "description": { + "en": "Proof that also remote licenses can be used with custom urls." + }, } - }] - response = self.client.post('/rest/datasets', self.cr_full_att_test_data, format="json") + ] + response = self.client.post("/rest/datasets", self.cr_full_att_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(len(response.data['research_dataset']['access_rights']['license'][0]), 2, response.data) - self.assertEqual(len(response.data['research_dataset']['remote_resources'][0]['license'][0]), 2, response.data) + self.assertEqual( + len(response.data["research_dataset"]["access_rights"]["license"][0]), + 2, + response.data, + ) + self.assertEqual( + len(response.data["research_dataset"]["remote_resources"][0]["license"][0]), + 2, + response.data, + ) def test_create_catalog_record_with_invalid_reference_data(self): - rd_ida = self.cr_full_ida_test_data['research_dataset'] - rd_ida['theme'][0]['identifier'] = 'nonexisting' - rd_ida['field_of_science'][0]['identifier'] = 'nonexisting' - rd_ida['language'][0]['identifier'] = 'nonexisting' - rd_ida['access_rights']['access_type']['identifier'] = 'nonexisting' - rd_ida['access_rights']['license'][0]['identifier'] = 'nonexisting' - rd_ida['other_identifier'][0]['type']['identifier'] = 'nonexisting' - rd_ida['spatial'][0]['place_uri']['identifier'] = 'nonexisting' - rd_ida['files'][0]['file_type']['identifier'] = 'nonexisting' - rd_ida['files'][0]['use_category']['identifier'] = 'nonexisting' - rd_ida['infrastructure'][0]['identifier'] = 'nonexisting' - rd_ida['creator'][0]['contributor_role'][0]['identifier'] = 'nonexisting' - rd_ida['curator'][0]['contributor_type'][0]['identifier'] = 'nonexisting' - rd_ida['is_output_of'][0]['funder_type']['identifier'] = 'nonexisting' - rd_ida['directories'][0]['use_category']['identifier'] = 'nonexisting' - rd_ida['relation'][0]['relation_type']['identifier'] = 'nonexisting' - rd_ida['relation'][0]['entity']['type']['identifier'] = 'nonexisting' - rd_ida['provenance'][0]['lifecycle_event']['identifier'] = 'nonexisting' - rd_ida['provenance'][1]['preservation_event']['identifier'] = 'nonexisting' - rd_ida['provenance'][0]['event_outcome']['identifier'] = 'nonexisting' - response = self.client.post('/rest/datasets', self.cr_full_ida_test_data, format="json") + rd_ida = self.cr_full_ida_test_data["research_dataset"] + rd_ida["theme"][0]["identifier"] = "nonexisting" + rd_ida["field_of_science"][0]["identifier"] = "nonexisting" + rd_ida["language"][0]["identifier"] = "nonexisting" + rd_ida["access_rights"]["access_type"]["identifier"] = "nonexisting" + rd_ida["access_rights"]["license"][0]["identifier"] = "nonexisting" + rd_ida["other_identifier"][0]["type"]["identifier"] = "nonexisting" + rd_ida["spatial"][0]["place_uri"]["identifier"] = "nonexisting" + rd_ida["files"][0]["file_type"]["identifier"] = "nonexisting" + rd_ida["files"][0]["use_category"]["identifier"] = "nonexisting" + rd_ida["infrastructure"][0]["identifier"] = "nonexisting" + rd_ida["creator"][0]["contributor_role"][0]["identifier"] = "nonexisting" + rd_ida["curator"][0]["contributor_type"][0]["identifier"] = "nonexisting" + rd_ida["is_output_of"][0]["funder_type"]["identifier"] = "nonexisting" + rd_ida["directories"][0]["use_category"]["identifier"] = "nonexisting" + rd_ida["relation"][0]["relation_type"]["identifier"] = "nonexisting" + rd_ida["relation"][0]["entity"]["type"]["identifier"] = "nonexisting" + rd_ida["provenance"][0]["lifecycle_event"]["identifier"] = "nonexisting" + rd_ida["provenance"][1]["preservation_event"]["identifier"] = "nonexisting" + rd_ida["provenance"][0]["event_outcome"]["identifier"] = "nonexisting" + response = self.client.post("/rest/datasets", self.cr_full_ida_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('research_dataset' in response.data.keys(), True) - self.assertEqual(len(response.data['research_dataset']), 19) - - rd_att = self.cr_full_att_test_data['research_dataset'] - rd_att['remote_resources'][0]['license'][0]['identifier'] = 'nonexisting' - rd_att['remote_resources'][1]['resource_type']['identifier'] = 'nonexisting' - rd_att['remote_resources'][0]['use_category']['identifier'] = 'nonexisting' - response = self.client.post('/rest/datasets', self.cr_full_att_test_data, format="json") + self.assertEqual("research_dataset" in response.data.keys(), True) + self.assertEqual(len(response.data["research_dataset"]), 19) + + rd_att = self.cr_full_att_test_data["research_dataset"] + rd_att["remote_resources"][0]["license"][0]["identifier"] = "nonexisting" + rd_att["remote_resources"][1]["resource_type"]["identifier"] = "nonexisting" + rd_att["remote_resources"][0]["use_category"]["identifier"] = "nonexisting" + response = self.client.post("/rest/datasets", self.cr_full_att_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('research_dataset' in response.data.keys(), True) - self.assertEqual(len(response.data['research_dataset']), 3) + self.assertEqual("research_dataset" in response.data.keys(), True) + self.assertEqual(len(response.data["research_dataset"]), 3) def test_create_catalog_record_populate_fields_from_reference_data(self): """ @@ -1497,99 +1817,111 @@ def test_create_catalog_record_populate_fields_from_reference_data(self): """ cache = RedisClient() rf = RDM.get_reference_data(cache) - refdata = rf['reference_data'] - orgdata = rf['organization_data'] + refdata = rf["reference_data"] + orgdata = rf["organization_data"] refs = {} data_types = [ - 'access_type', - 'restriction_grounds', - 'field_of_science', - 'identifier_type', - 'keyword', - 'language', - 'license', - 'location', - 'resource_type', - 'file_type', - 'use_category', - 'research_infra', - 'contributor_role', - 'contributor_type', - 'funder_type', - 'relation_type', - 'lifecycle_event', - 'preservation_event', - 'event_outcome' + "access_type", + "restriction_grounds", + "field_of_science", + "identifier_type", + "keyword", + "language", + "license", + "location", + "resource_type", + "file_type", + "use_category", + "research_infra", + "contributor_role", + "contributor_type", + "funder_type", + "relation_type", + "lifecycle_event", + "preservation_event", + "event_outcome", ] # the values in these selected entries will be used throghout the rest of the test case for dtype in data_types: - if dtype == 'location': - entry = next((obj for obj in refdata[dtype] if obj.get('wkt', False)), None) + if dtype == "location": + entry = next((obj for obj in refdata[dtype] if obj.get("wkt", False)), None) self.assertTrue(entry is not None) else: entry = refdata[dtype][1] refs[dtype] = { - 'code': entry['code'], - 'uri': entry['uri'], - 'label': entry.get('label', None), - 'wkt': entry.get('wkt', None), - 'scheme': entry.get('scheme', None) + "code": entry["code"], + "uri": entry["uri"], + "label": entry.get("label", None), + "wkt": entry.get("wkt", None), + "scheme": entry.get("scheme", None), } - refs['organization'] = { - 'uri': orgdata['organization'][0]['uri'], - 'code': orgdata['organization'][0]['code'], - 'label': orgdata['organization'][0]['label'] + refs["organization"] = { + "uri": orgdata["organization"][0]["uri"], + "code": orgdata["organization"][0]["code"], + "label": orgdata["organization"][0]["label"], } # replace the relations with objects that have only the identifier set with code as value, # to easily check that label was populated (= that it appeared in the dataset after create) # without knowing its original value from the generated test data - rd_ida = self.cr_full_ida_test_data['research_dataset'] - rd_ida['theme'][0] = {'identifier': refs['keyword']['code']} - rd_ida['field_of_science'][0] = {'identifier': refs['field_of_science']['code']} - rd_ida['language'][0] = {'identifier': refs['language']['code']} - rd_ida['access_rights']['access_type'] = {'identifier': refs['access_type']['code']} - rd_ida['access_rights']['restriction_grounds'][0] = {'identifier': refs['restriction_grounds']['code']} - rd_ida['access_rights']['license'][0] = {'identifier': refs['license']['code']} - rd_ida['other_identifier'][0]['type'] = {'identifier': refs['identifier_type']['code']} - rd_ida['spatial'][0]['place_uri'] = {'identifier': refs['location']['code']} - rd_ida['files'][0]['file_type'] = {'identifier': refs['file_type']['code']} - rd_ida['files'][0]['use_category'] = {'identifier': refs['use_category']['code']} - rd_ida['directories'][0]['use_category'] = {'identifier': refs['use_category']['code']} - rd_ida['infrastructure'][0] = {'identifier': refs['research_infra']['code']} - rd_ida['creator'][0]['contributor_role'][0] = {'identifier': refs['contributor_role']['code']} - rd_ida['curator'][0]['contributor_type'][0] = {'identifier': refs['contributor_type']['code']} - rd_ida['is_output_of'][0]['funder_type'] = {'identifier': refs['funder_type']['code']} - rd_ida['relation'][0]['relation_type'] = {'identifier': refs['relation_type']['code']} - rd_ida['relation'][0]['entity']['type'] = {'identifier': refs['resource_type']['code']} - rd_ida['provenance'][0]['lifecycle_event'] = {'identifier': refs['lifecycle_event']['code']} - rd_ida['provenance'][1]['preservation_event'] = {'identifier': refs['preservation_event']['code']} - rd_ida['provenance'][0]['event_outcome'] = {'identifier': refs['event_outcome']['code']} + rd_ida = self.cr_full_ida_test_data["research_dataset"] + rd_ida["theme"][0] = {"identifier": refs["keyword"]["code"]} + rd_ida["field_of_science"][0] = {"identifier": refs["field_of_science"]["code"]} + rd_ida["language"][0] = {"identifier": refs["language"]["code"]} + rd_ida["access_rights"]["access_type"] = {"identifier": refs["access_type"]["code"]} + rd_ida["access_rights"]["restriction_grounds"][0] = { + "identifier": refs["restriction_grounds"]["code"] + } + rd_ida["access_rights"]["license"][0] = {"identifier": refs["license"]["code"]} + rd_ida["other_identifier"][0]["type"] = {"identifier": refs["identifier_type"]["code"]} + rd_ida["spatial"][0]["place_uri"] = {"identifier": refs["location"]["code"]} + rd_ida["files"][0]["file_type"] = {"identifier": refs["file_type"]["code"]} + rd_ida["files"][0]["use_category"] = {"identifier": refs["use_category"]["code"]} + rd_ida["directories"][0]["use_category"] = {"identifier": refs["use_category"]["code"]} + rd_ida["infrastructure"][0] = {"identifier": refs["research_infra"]["code"]} + rd_ida["creator"][0]["contributor_role"][0] = { + "identifier": refs["contributor_role"]["code"] + } + rd_ida["curator"][0]["contributor_type"][0] = { + "identifier": refs["contributor_type"]["code"] + } + rd_ida["is_output_of"][0]["funder_type"] = {"identifier": refs["funder_type"]["code"]} + rd_ida["relation"][0]["relation_type"] = {"identifier": refs["relation_type"]["code"]} + rd_ida["relation"][0]["entity"]["type"] = {"identifier": refs["resource_type"]["code"]} + rd_ida["provenance"][0]["lifecycle_event"] = {"identifier": refs["lifecycle_event"]["code"]} + rd_ida["provenance"][1]["preservation_event"] = { + "identifier": refs["preservation_event"]["code"] + } + rd_ida["provenance"][0]["event_outcome"] = {"identifier": refs["event_outcome"]["code"]} # these have other required fields, so only update the identifier with code - rd_ida['is_output_of'][0]['source_organization'][0]['identifier'] = refs['organization']['code'] - rd_ida['is_output_of'][0]['has_funding_agency'][0]['identifier'] = refs['organization']['code'] - rd_ida['other_identifier'][0]['provider']['identifier'] = refs['organization']['code'] - rd_ida['contributor'][0]['member_of']['identifier'] = refs['organization']['code'] - rd_ida['creator'][0]['member_of']['identifier'] = refs['organization']['code'] - rd_ida['curator'][0]['is_part_of']['identifier'] = refs['organization']['code'] - rd_ida['publisher']['is_part_of']['identifier'] = refs['organization']['code'] - rd_ida['rights_holder'][0]['is_part_of']['identifier'] = refs['organization']['code'] + rd_ida["is_output_of"][0]["source_organization"][0]["identifier"] = refs["organization"][ + "code" + ] + rd_ida["is_output_of"][0]["has_funding_agency"][0]["identifier"] = refs["organization"][ + "code" + ] + rd_ida["other_identifier"][0]["provider"]["identifier"] = refs["organization"]["code"] + rd_ida["contributor"][0]["member_of"]["identifier"] = refs["organization"]["code"] + rd_ida["creator"][0]["member_of"]["identifier"] = refs["organization"]["code"] + rd_ida["curator"][0]["is_part_of"]["identifier"] = refs["organization"]["code"] + rd_ida["publisher"]["is_part_of"]["identifier"] = refs["organization"]["code"] + rd_ida["rights_holder"][0]["is_part_of"]["identifier"] = refs["organization"]["code"] # Other type of reference data populations - orig_wkt_value = rd_ida['spatial'][0]['as_wkt'][0] - rd_ida['spatial'][0]['place_uri']['identifier'] = refs['location']['code'] - rd_ida['spatial'][1]['as_wkt'] = [] - rd_ida['spatial'][1]['place_uri']['identifier'] = refs['location']['code'] + orig_wkt_value = rd_ida["spatial"][0]["as_wkt"][0] + rd_ida["spatial"][0]["place_uri"]["identifier"] = refs["location"]["code"] + rd_ida["spatial"][1]["as_wkt"] = [] + rd_ida["spatial"][1]["place_uri"]["identifier"] = refs["location"]["code"] - response = self.client.post('/rest/datasets', self.cr_full_ida_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_full_ida_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('research_dataset' in response.data.keys(), True) + self.assertEqual("research_dataset" in response.data.keys(), True) - new_rd_ida = response.data['research_dataset'] + new_rd_ida = response.data["research_dataset"] self._assert_uri_copied_to_identifier(refs, new_rd_ida) self._assert_label_copied_to_pref_label(refs, new_rd_ida) self._assert_label_copied_to_title(refs, new_rd_ida) @@ -1597,170 +1929,382 @@ def test_create_catalog_record_populate_fields_from_reference_data(self): # Assert if spatial as_wkt field has been populated with a value from ref data which has wkt value having # condition that the user has not given own coordinates in the as_wkt field - self.assertEqual(orig_wkt_value, new_rd_ida['spatial'][0]['as_wkt'][0]) - self.assertEqual(refs['location']['wkt'], new_rd_ida['spatial'][1]['as_wkt'][0]) + self.assertEqual(orig_wkt_value, new_rd_ida["spatial"][0]["as_wkt"][0]) + self.assertEqual(refs["location"]["wkt"], new_rd_ida["spatial"][1]["as_wkt"][0]) # rd from att data catalog - rd_att = self.cr_full_att_test_data['research_dataset'] - rd_att['remote_resources'][1]['resource_type'] = {'identifier': refs['resource_type']['code']} - rd_att['remote_resources'][0]['use_category'] = {'identifier': refs['use_category']['code']} - rd_att['remote_resources'][0]['license'][0] = {'identifier': refs['license']['code']} + rd_att = self.cr_full_att_test_data["research_dataset"] + rd_att["remote_resources"][1]["resource_type"] = { + "identifier": refs["resource_type"]["code"] + } + rd_att["remote_resources"][0]["use_category"] = {"identifier": refs["use_category"]["code"]} + rd_att["remote_resources"][0]["license"][0] = {"identifier": refs["license"]["code"]} # Assert remote resources related reference datas - response = self.client.post('/rest/datasets', self.cr_full_att_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_full_att_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('research_dataset' in response.data.keys(), True) - new_rd_att = response.data['research_dataset'] + self.assertEqual("research_dataset" in response.data.keys(), True) + new_rd_att = response.data["research_dataset"] self._assert_att_remote_resource_items(refs, new_rd_att) def _assert_att_remote_resource_items(self, refs, new_rd): - self.assertEqual(refs['resource_type']['uri'], new_rd['remote_resources'][1]['resource_type']['identifier']) - self.assertEqual(refs['use_category']['uri'], new_rd['remote_resources'][0]['use_category']['identifier']) - self.assertEqual(refs['license']['uri'], new_rd['remote_resources'][0]['license'][0]['identifier']) - self.assertEqual(refs['resource_type']['label'], - new_rd['remote_resources'][1]['resource_type'].get('pref_label', None)) - self.assertEqual(refs['use_category']['label'], - new_rd['remote_resources'][0]['use_category'].get('pref_label', None)) - self.assertEqual(refs['license']['label'], new_rd['remote_resources'][0]['license'][0].get('title', None)) + self.assertEqual( + refs["resource_type"]["uri"], + new_rd["remote_resources"][1]["resource_type"]["identifier"], + ) + self.assertEqual( + refs["use_category"]["uri"], + new_rd["remote_resources"][0]["use_category"]["identifier"], + ) + self.assertEqual( + refs["license"]["uri"], + new_rd["remote_resources"][0]["license"][0]["identifier"], + ) + self.assertEqual( + refs["resource_type"]["label"], + new_rd["remote_resources"][1]["resource_type"].get("pref_label", None), + ) + self.assertEqual( + refs["use_category"]["label"], + new_rd["remote_resources"][0]["use_category"].get("pref_label", None), + ) + self.assertEqual( + refs["license"]["label"], + new_rd["remote_resources"][0]["license"][0].get("title", None), + ) def _assert_uri_copied_to_identifier(self, refs, new_rd): - self.assertEqual(refs['keyword']['uri'], new_rd['theme'][0]['identifier']) - self.assertEqual(refs['field_of_science']['uri'], new_rd['field_of_science'][0]['identifier']) - self.assertEqual(refs['language']['uri'], new_rd['language'][0]['identifier']) - self.assertEqual(refs['access_type']['uri'], new_rd['access_rights']['access_type']['identifier']) - self.assertEqual(refs['restriction_grounds']['uri'], - new_rd['access_rights']['restriction_grounds'][0]['identifier']) - self.assertEqual(refs['license']['uri'], new_rd['access_rights']['license'][0]['identifier']) - self.assertEqual(refs['identifier_type']['uri'], new_rd['other_identifier'][0]['type']['identifier']) - self.assertEqual(refs['location']['uri'], new_rd['spatial'][0]['place_uri']['identifier']) - self.assertEqual(refs['file_type']['uri'], new_rd['files'][0]['file_type']['identifier']) - self.assertEqual(refs['use_category']['uri'], new_rd['files'][0]['use_category']['identifier']) - - self.assertEqual(refs['use_category']['uri'], new_rd['directories'][0]['use_category']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['is_output_of'][0]['source_organization'][0]['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['is_output_of'][0]['has_funding_agency'][0]['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['other_identifier'][0]['provider']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['contributor'][0]['member_of']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['creator'][0]['member_of']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['curator'][0]['is_part_of']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['publisher']['is_part_of']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['rights_holder'][0]['is_part_of']['identifier']) - self.assertEqual(refs['research_infra']['uri'], new_rd['infrastructure'][0]['identifier']) - self.assertEqual(refs['contributor_role']['uri'], new_rd['creator'][0]['contributor_role'][0]['identifier']) - self.assertEqual(refs['contributor_type']['uri'], new_rd['curator'][0]['contributor_type'][0]['identifier']) - self.assertEqual(refs['funder_type']['uri'], new_rd['is_output_of'][0]['funder_type']['identifier']) - self.assertEqual(refs['relation_type']['uri'], new_rd['relation'][0]['relation_type']['identifier']) - self.assertEqual(refs['resource_type']['uri'], new_rd['relation'][0]['entity']['type']['identifier']) - self.assertEqual(refs['lifecycle_event']['uri'], new_rd['provenance'][0]['lifecycle_event']['identifier']) - self.assertEqual(refs['preservation_event']['uri'], new_rd['provenance'][1]['preservation_event']['identifier']) - self.assertEqual(refs['event_outcome']['uri'], new_rd['provenance'][0]['event_outcome']['identifier']) + self.assertEqual(refs["keyword"]["uri"], new_rd["theme"][0]["identifier"]) + self.assertEqual( + refs["field_of_science"]["uri"], new_rd["field_of_science"][0]["identifier"] + ) + self.assertEqual(refs["language"]["uri"], new_rd["language"][0]["identifier"]) + self.assertEqual( + refs["access_type"]["uri"], + new_rd["access_rights"]["access_type"]["identifier"], + ) + self.assertEqual( + refs["restriction_grounds"]["uri"], + new_rd["access_rights"]["restriction_grounds"][0]["identifier"], + ) + self.assertEqual( + refs["license"]["uri"], new_rd["access_rights"]["license"][0]["identifier"] + ) + self.assertEqual( + refs["identifier_type"]["uri"], + new_rd["other_identifier"][0]["type"]["identifier"], + ) + self.assertEqual(refs["location"]["uri"], new_rd["spatial"][0]["place_uri"]["identifier"]) + self.assertEqual(refs["file_type"]["uri"], new_rd["files"][0]["file_type"]["identifier"]) + self.assertEqual( + refs["use_category"]["uri"], + new_rd["files"][0]["use_category"]["identifier"], + ) + + self.assertEqual( + refs["use_category"]["uri"], + new_rd["directories"][0]["use_category"]["identifier"], + ) + self.assertEqual( + refs["organization"]["uri"], + new_rd["is_output_of"][0]["source_organization"][0]["identifier"], + ) + self.assertEqual( + refs["organization"]["uri"], + new_rd["is_output_of"][0]["has_funding_agency"][0]["identifier"], + ) + self.assertEqual( + refs["organization"]["uri"], + new_rd["other_identifier"][0]["provider"]["identifier"], + ) + self.assertEqual( + refs["organization"]["uri"], + new_rd["contributor"][0]["member_of"]["identifier"], + ) + self.assertEqual( + refs["organization"]["uri"], new_rd["creator"][0]["member_of"]["identifier"] + ) + self.assertEqual( + refs["organization"]["uri"], + new_rd["curator"][0]["is_part_of"]["identifier"], + ) + self.assertEqual( + refs["organization"]["uri"], new_rd["publisher"]["is_part_of"]["identifier"] + ) + self.assertEqual( + refs["organization"]["uri"], + new_rd["rights_holder"][0]["is_part_of"]["identifier"], + ) + self.assertEqual(refs["research_infra"]["uri"], new_rd["infrastructure"][0]["identifier"]) + self.assertEqual( + refs["contributor_role"]["uri"], + new_rd["creator"][0]["contributor_role"][0]["identifier"], + ) + self.assertEqual( + refs["contributor_type"]["uri"], + new_rd["curator"][0]["contributor_type"][0]["identifier"], + ) + self.assertEqual( + refs["funder_type"]["uri"], + new_rd["is_output_of"][0]["funder_type"]["identifier"], + ) + self.assertEqual( + refs["relation_type"]["uri"], + new_rd["relation"][0]["relation_type"]["identifier"], + ) + self.assertEqual( + refs["resource_type"]["uri"], + new_rd["relation"][0]["entity"]["type"]["identifier"], + ) + self.assertEqual( + refs["lifecycle_event"]["uri"], + new_rd["provenance"][0]["lifecycle_event"]["identifier"], + ) + self.assertEqual( + refs["preservation_event"]["uri"], + new_rd["provenance"][1]["preservation_event"]["identifier"], + ) + self.assertEqual( + refs["event_outcome"]["uri"], + new_rd["provenance"][0]["event_outcome"]["identifier"], + ) def _assert_scheme_copied_to_in_scheme(self, refs, new_rd): - self.assertEqual(refs['keyword']['scheme'], new_rd['theme'][0]['in_scheme']) - self.assertEqual(refs['field_of_science']['scheme'], new_rd['field_of_science'][0]['in_scheme']) - self.assertEqual(refs['language']['scheme'], new_rd['language'][0]['in_scheme']) - self.assertEqual(refs['access_type']['scheme'], new_rd['access_rights']['access_type']['in_scheme']) - self.assertEqual(refs['restriction_grounds']['scheme'], - new_rd['access_rights']['restriction_grounds'][0]['in_scheme']) - self.assertEqual(refs['license']['scheme'], new_rd['access_rights']['license'][0]['in_scheme']) - self.assertEqual(refs['identifier_type']['scheme'], new_rd['other_identifier'][0]['type']['in_scheme']) - self.assertEqual(refs['location']['scheme'], new_rd['spatial'][0]['place_uri']['in_scheme']) - self.assertEqual(refs['file_type']['scheme'], new_rd['files'][0]['file_type']['in_scheme']) - self.assertEqual(refs['use_category']['scheme'], new_rd['files'][0]['use_category']['in_scheme']) - - self.assertEqual(refs['use_category']['scheme'], new_rd['directories'][0]['use_category']['in_scheme']) - self.assertEqual(refs['research_infra']['scheme'], new_rd['infrastructure'][0]['in_scheme']) - self.assertEqual(refs['contributor_role']['scheme'], new_rd['creator'][0]['contributor_role']['in_scheme']) - self.assertEqual(refs['contributor_type']['scheme'], new_rd['curator'][0]['contributor_type']['in_scheme']) - self.assertEqual(refs['funder_type']['scheme'], new_rd['is_output_of'][0]['funder_type']['in_scheme']) - self.assertEqual(refs['relation_type']['scheme'], new_rd['relation'][0]['relation_type']['in_scheme']) - self.assertEqual(refs['resource_type']['scheme'], new_rd['relation'][0]['entity']['type']['in_scheme']) - self.assertEqual(refs['lifecycle_event']['scheme'], new_rd['provenance'][0]['lifecycle_event']['in_scheme']) - self.assertEqual(refs['preservation_event']['scheme'], - new_rd['provenance'][1]['preservation_event']['in_scheme']) - self.assertEqual(refs['event_outcome']['scheme'], new_rd['provenance'][0]['event_outcome']['in_scheme']) + self.assertEqual(refs["keyword"]["scheme"], new_rd["theme"][0]["in_scheme"]) + self.assertEqual( + refs["field_of_science"]["scheme"], + new_rd["field_of_science"][0]["in_scheme"], + ) + self.assertEqual(refs["language"]["scheme"], new_rd["language"][0]["in_scheme"]) + self.assertEqual( + refs["access_type"]["scheme"], + new_rd["access_rights"]["access_type"]["in_scheme"], + ) + self.assertEqual( + refs["restriction_grounds"]["scheme"], + new_rd["access_rights"]["restriction_grounds"][0]["in_scheme"], + ) + self.assertEqual( + refs["license"]["scheme"], + new_rd["access_rights"]["license"][0]["in_scheme"], + ) + self.assertEqual( + refs["identifier_type"]["scheme"], + new_rd["other_identifier"][0]["type"]["in_scheme"], + ) + self.assertEqual(refs["location"]["scheme"], new_rd["spatial"][0]["place_uri"]["in_scheme"]) + self.assertEqual(refs["file_type"]["scheme"], new_rd["files"][0]["file_type"]["in_scheme"]) + self.assertEqual( + refs["use_category"]["scheme"], + new_rd["files"][0]["use_category"]["in_scheme"], + ) + + self.assertEqual( + refs["use_category"]["scheme"], + new_rd["directories"][0]["use_category"]["in_scheme"], + ) + self.assertEqual(refs["research_infra"]["scheme"], new_rd["infrastructure"][0]["in_scheme"]) + self.assertEqual( + refs["contributor_role"]["scheme"], + new_rd["creator"][0]["contributor_role"]["in_scheme"], + ) + self.assertEqual( + refs["contributor_type"]["scheme"], + new_rd["curator"][0]["contributor_type"]["in_scheme"], + ) + self.assertEqual( + refs["funder_type"]["scheme"], + new_rd["is_output_of"][0]["funder_type"]["in_scheme"], + ) + self.assertEqual( + refs["relation_type"]["scheme"], + new_rd["relation"][0]["relation_type"]["in_scheme"], + ) + self.assertEqual( + refs["resource_type"]["scheme"], + new_rd["relation"][0]["entity"]["type"]["in_scheme"], + ) + self.assertEqual( + refs["lifecycle_event"]["scheme"], + new_rd["provenance"][0]["lifecycle_event"]["in_scheme"], + ) + self.assertEqual( + refs["preservation_event"]["scheme"], + new_rd["provenance"][1]["preservation_event"]["in_scheme"], + ) + self.assertEqual( + refs["event_outcome"]["scheme"], + new_rd["provenance"][0]["event_outcome"]["in_scheme"], + ) + + def _assert_label_copied_to_pref_label(self, refs, new_rd): + self.assertEqual(refs["keyword"]["label"], new_rd["theme"][0].get("pref_label", None)) + self.assertEqual( + refs["field_of_science"]["label"], + new_rd["field_of_science"][0].get("pref_label", None), + ) + self.assertEqual( + refs["access_type"]["label"], + new_rd["access_rights"]["access_type"].get("pref_label", None), + ) + self.assertEqual( + refs["restriction_grounds"]["label"], + new_rd["access_rights"]["restriction_grounds"][0].get("pref_label", None), + ) + self.assertEqual( + refs["identifier_type"]["label"], + new_rd["other_identifier"][0]["type"].get("pref_label", None), + ) + self.assertEqual( + refs["location"]["label"], + new_rd["spatial"][0]["place_uri"].get("pref_label", None), + ) + self.assertEqual( + refs["file_type"]["label"], + new_rd["files"][0]["file_type"].get("pref_label", None), + ) + self.assertEqual( + refs["use_category"]["label"], + new_rd["files"][0]["use_category"].get("pref_label", None), + ) + self.assertEqual( + refs["use_category"]["label"], + new_rd["directories"][0]["use_category"].get("pref_label", None), + ) - def _assert_label_copied_to_pref_label(self, refs, new_rd): - self.assertEqual(refs['keyword']['label'], new_rd['theme'][0].get('pref_label', None)) - self.assertEqual(refs['field_of_science']['label'], new_rd['field_of_science'][0].get('pref_label', None)) - self.assertEqual(refs['access_type']['label'], new_rd['access_rights']['access_type'].get('pref_label', None)) - self.assertEqual(refs['restriction_grounds']['label'], - new_rd['access_rights']['restriction_grounds'][0].get('pref_label', None)) - self.assertEqual(refs['identifier_type']['label'], - new_rd['other_identifier'][0]['type'].get('pref_label', None)) - self.assertEqual(refs['location']['label'], new_rd['spatial'][0]['place_uri'].get('pref_label', None)) - self.assertEqual(refs['file_type']['label'], new_rd['files'][0]['file_type'].get('pref_label', None)) - self.assertEqual(refs['use_category']['label'], new_rd['files'][0]['use_category'].get('pref_label', None)) - self.assertEqual(refs['use_category']['label'], - new_rd['directories'][0]['use_category'].get('pref_label', None)) - - self.assertEqual(refs['research_infra']['label'], new_rd['infrastructure'][0].get('pref_label', None)) - self.assertEqual(refs['contributor_role']['label'], - new_rd['creator'][0]['contributor_role'][0].get('pref_label', None)) - self.assertEqual(refs['contributor_type']['label'], - new_rd['curator'][0]['contributor_type'][0].get('pref_label', None)) - self.assertEqual(refs['funder_type']['label'], new_rd['is_output_of'][0]['funder_type'].get('pref_label', None)) - self.assertEqual(refs['relation_type']['label'], new_rd['relation'][0]['relation_type'].get('pref_label', None)) - self.assertEqual(refs['resource_type']['label'], - new_rd['relation'][0]['entity']['type'].get('pref_label', None)) - self.assertEqual(refs['lifecycle_event']['label'], - new_rd['provenance'][0]['lifecycle_event'].get('pref_label', None)) - self.assertEqual(refs['preservation_event']['label'], - new_rd['provenance'][1]['preservation_event'].get('pref_label', None)) - self.assertEqual(refs['event_outcome']['label'], - new_rd['provenance'][0]['event_outcome'].get('pref_label', None)) + self.assertEqual( + refs["research_infra"]["label"], + new_rd["infrastructure"][0].get("pref_label", None), + ) + self.assertEqual( + refs["contributor_role"]["label"], + new_rd["creator"][0]["contributor_role"][0].get("pref_label", None), + ) + self.assertEqual( + refs["contributor_type"]["label"], + new_rd["curator"][0]["contributor_type"][0].get("pref_label", None), + ) + self.assertEqual( + refs["funder_type"]["label"], + new_rd["is_output_of"][0]["funder_type"].get("pref_label", None), + ) + self.assertEqual( + refs["relation_type"]["label"], + new_rd["relation"][0]["relation_type"].get("pref_label", None), + ) + self.assertEqual( + refs["resource_type"]["label"], + new_rd["relation"][0]["entity"]["type"].get("pref_label", None), + ) + self.assertEqual( + refs["lifecycle_event"]["label"], + new_rd["provenance"][0]["lifecycle_event"].get("pref_label", None), + ) + self.assertEqual( + refs["preservation_event"]["label"], + new_rd["provenance"][1]["preservation_event"].get("pref_label", None), + ) + self.assertEqual( + refs["event_outcome"]["label"], + new_rd["provenance"][0]["event_outcome"].get("pref_label", None), + ) def _assert_label_copied_to_title(self, refs, new_rd): - required_langs = dict((lang, val) for lang, val in refs['language']['label'].items() - if lang in ['fi', 'sv', 'en', 'und']) - self.assertEqual(required_langs, new_rd['language'][0].get('title', None)) - self.assertEqual(refs['license']['label'], new_rd['access_rights']['license'][0].get('title', None)) + required_langs = dict( + (lang, val) + for lang, val in refs["language"]["label"].items() + if lang in ["fi", "sv", "en", "und"] + ) + self.assertEqual(required_langs, new_rd["language"][0].get("title", None)) + self.assertEqual( + refs["license"]["label"], + new_rd["access_rights"]["license"][0].get("title", None), + ) def _assert_label_copied_to_name(self, refs, new_rd): - self.assertEqual(refs['organization']['label'], - new_rd['is_output_of'][0]['source_organization'][0].get('name', None)) - self.assertEqual(refs['organization']['label'], - new_rd['is_output_of'][0]['has_funding_agency'][0].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['other_identifier'][0]['provider'].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['contributor'][0]['member_of'].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['creator'][0]['member_of'].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['curator'][0]['is_part_of'].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['publisher']['is_part_of'].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['rights_holder'][0]['is_part_of'].get('name', None)) + self.assertEqual( + refs["organization"]["label"], + new_rd["is_output_of"][0]["source_organization"][0].get("name", None), + ) + self.assertEqual( + refs["organization"]["label"], + new_rd["is_output_of"][0]["has_funding_agency"][0].get("name", None), + ) + self.assertEqual( + refs["organization"]["label"], + new_rd["other_identifier"][0]["provider"].get("name", None), + ) + self.assertEqual( + refs["organization"]["label"], + new_rd["contributor"][0]["member_of"].get("name", None), + ) + self.assertEqual( + refs["organization"]["label"], + new_rd["creator"][0]["member_of"].get("name", None), + ) + self.assertEqual( + refs["organization"]["label"], + new_rd["curator"][0]["is_part_of"].get("name", None), + ) + self.assertEqual( + refs["organization"]["label"], + new_rd["publisher"]["is_part_of"].get("name", None), + ) + self.assertEqual( + refs["organization"]["label"], + new_rd["rights_holder"][0]["is_part_of"].get("name", None), + ) def test_refdata_sub_org_main_org_population(self): # Test parent org gets populated when sub org is from ref data and user has not provided is_part_of relation - self.cr_test_data['research_dataset']['publisher'] = {'@type': 'Organization', 'identifier': '10076-A800'} - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["publisher"] = { + "@type": "Organization", + "identifier": "10076-A800", + } + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('is_part_of' in response.data['research_dataset']['publisher'], True) - self.assertEqual('http://uri.suomi.fi/codelist/fairdata/organization/code/10076', - response.data['research_dataset']['publisher']['is_part_of']['identifier']) - self.assertTrue(response.data['research_dataset']['publisher']['is_part_of'].get('name', False)) + self.assertEqual("is_part_of" in response.data["research_dataset"]["publisher"], True) + self.assertEqual( + "http://uri.suomi.fi/codelist/fairdata/organization/code/10076", + response.data["research_dataset"]["publisher"]["is_part_of"]["identifier"], + ) + self.assertTrue( + response.data["research_dataset"]["publisher"]["is_part_of"].get("name", False) + ) # Test parent org does not get populated when sub org is from ref data and user has provided is_part_of relation - self.cr_test_data['research_dataset']['publisher'] = { - '@type': 'Organization', - 'identifier': '10076-A800', - 'is_part_of': { - '@type': 'Organization', - 'identifier': 'test_id', - 'name': {'und': 'test_name'} - }} - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["publisher"] = { + "@type": "Organization", + "identifier": "10076-A800", + "is_part_of": { + "@type": "Organization", + "identifier": "test_id", + "name": {"und": "test_name"}, + }, + } + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('is_part_of' in response.data['research_dataset']['publisher'], True) - self.assertEqual('test_id', response.data['research_dataset']['publisher']['is_part_of']['identifier']) - self.assertEqual('test_name', response.data['research_dataset']['publisher']['is_part_of']['name']['und']) + self.assertEqual("is_part_of" in response.data["research_dataset"]["publisher"], True) + self.assertEqual( + "test_id", + response.data["research_dataset"]["publisher"]["is_part_of"]["identifier"], + ) + self.assertEqual( + "test_name", + response.data["research_dataset"]["publisher"]["is_part_of"]["name"]["und"], + ) # Test nothing happens when org is a parent org - self.cr_test_data['research_dataset']['publisher'] = {'@type': 'Organization', 'identifier': '10076'} - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["publisher"] = { + "@type": "Organization", + "identifier": "10076", + } + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('is_part_of' not in response.data['research_dataset']['publisher'], True) + self.assertEqual("is_part_of" not in response.data["research_dataset"]["publisher"], True) class CatalogRecordApiWriteAlternateRecords(CatalogRecordApiWriteCommon): @@ -1777,8 +2321,8 @@ class CatalogRecordApiWriteAlternateRecords(CatalogRecordApiWriteCommon): def setUp(self): super(CatalogRecordApiWriteAlternateRecords, self).setUp() self.preferred_identifier = self._set_preferred_identifier_to_record(pk=1, data_catalog=1) - self.cr_test_data['research_dataset']['preferred_identifier'] = self.preferred_identifier - self.cr_test_data['data_catalog'] = None + self.cr_test_data["research_dataset"]["preferred_identifier"] = self.preferred_identifier + self.cr_test_data["data_catalog"] = None def test_alternate_record_set_is_created_if_it_doesnt_exist(self): """ @@ -1787,21 +2331,29 @@ def test_alternate_record_set_is_created_if_it_doesnt_exist(self): """ # new record is saved to catalog 3, which does not support versioning - self.cr_test_data['data_catalog'] = 3 + self.cr_test_data["data_catalog"] = 3 existing_records_count = CatalogRecord.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}).count() - self.assertEqual(existing_records_count, 1, - 'in the beginning, there should be only one record with pref id %s' - % self.preferred_identifier) + research_dataset__contains={"preferred_identifier": self.preferred_identifier} + ).count() + self.assertEqual( + existing_records_count, + 1, + "in the beginning, there should be only one record with pref id %s" + % self.preferred_identifier, + ) - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) records = CatalogRecord.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}) - self.assertEqual(len(records), 2, - 'after, there should be two records with pref id %s' % self.preferred_identifier) + research_dataset__contains={"preferred_identifier": self.preferred_identifier} + ) + self.assertEqual( + len(records), + 2, + "after, there should be two records with pref id %s" % self.preferred_identifier, + ) # both records are moved to same set ars_id = records[0].alternate_record_set.id @@ -1810,7 +2362,7 @@ def test_alternate_record_set_is_created_if_it_doesnt_exist(self): # records in the set are the ones expected self.assertEqual(records[0].id, 1) - self.assertEqual(records[1].id, response.data['id']) + self.assertEqual(records[1].id, response.data["id"]) # records in the set are indeed in different catalogs self.assertEqual(records[0].data_catalog.id, 1) @@ -1823,20 +2375,29 @@ def test_append_to_existing_alternate_record_set_if_it_exists(self): to the existing alternate_record_set. """ self._set_preferred_identifier_to_record(pk=2, data_catalog=2) - self.cr_test_data['data_catalog'] = 3 + self.cr_test_data["data_catalog"] = 3 existing_records_count = CatalogRecord.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}).count() - self.assertEqual(existing_records_count, 2, - 'in the beginning, there should be two records with pref id %s' % self.preferred_identifier) + research_dataset__contains={"preferred_identifier": self.preferred_identifier} + ).count() + self.assertEqual( + existing_records_count, + 2, + "in the beginning, there should be two records with pref id %s" + % self.preferred_identifier, + ) - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) records = CatalogRecord.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}) - self.assertEqual(len(records), 3, - 'after, there should be three records with pref id %s' % self.preferred_identifier) + research_dataset__contains={"preferred_identifier": self.preferred_identifier} + ) + self.assertEqual( + len(records), + 3, + "after, there should be three records with pref id %s" % self.preferred_identifier, + ) # all records belong to same set ars_id = records[0].alternate_record_set.id @@ -1847,7 +2408,7 @@ def test_append_to_existing_alternate_record_set_if_it_exists(self): # records in the set are the ones expected self.assertEqual(records[0].id, 1) self.assertEqual(records[1].id, 2) - self.assertEqual(records[2].id, response.data['id']) + self.assertEqual(records[2].id, response.data["id"]) # records in the set are indeed in different catalogs self.assertEqual(records[0].data_catalog.id, 1) @@ -1864,15 +2425,18 @@ def test_record_is_removed_from_alternate_record_set_when_deleted(self): # initial conditions will have 3 records in the same set. self._set_and_ensure_initial_conditions() - response = self.client.delete('/rest/datasets/2', format="json") + response = self.client.delete("/rest/datasets/2", format="json") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) # check resulting conditions records = CatalogRecord.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}) + research_dataset__contains={"preferred_identifier": self.preferred_identifier} + ) self.assertEqual(records[0].alternate_record_set.records.count(), 2) - def test_alternate_record_set_is_deleted_if_updating_record_with_no_versioning_and_one_record_left(self): + def test_alternate_record_set_is_deleted_if_updating_record_with_no_versioning_and_one_record_left( + self, + ): """ Same as above, but updating a record in a catalog, which does NOT support versioning. In this case, the the records itself gets updated, and removed from the old alternate_record_set. @@ -1889,22 +2453,28 @@ def test_alternate_record_set_is_deleted_if_updating_record_with_no_versioning_a old_ars_id = CatalogRecord.objects.get(pk=2).alternate_record_set.id # retrieve record id=2, and change its preferred identifier - response = self.client.get('/rest/datasets/2', format="json") - data = {'research_dataset': response.data['research_dataset']} - data['research_dataset']['preferred_identifier'] = 'a:new:identifier:here' + response = self.client.get("/rest/datasets/2", format="json") + data = {"research_dataset": response.data["research_dataset"]} + data["research_dataset"]["preferred_identifier"] = "a:new:identifier:here" # updating preferred_identifier - a new version is NOT created - response = self.client.patch('/rest/datasets/2', data=data, format="json") + response = self.client.patch("/rest/datasets/2", data=data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) records = CatalogRecord.objects.filter( - research_dataset__contains={'preferred_identifier': original_preferred_identifier }) + research_dataset__contains={"preferred_identifier": original_preferred_identifier} + ) self.assertEqual(records.count(), 1) - with self.assertRaises(AlternateRecordSet.DoesNotExist, msg='alternate record set should have been deleted'): + with self.assertRaises( + AlternateRecordSet.DoesNotExist, + msg="alternate record set should have been deleted", + ): AlternateRecordSet.objects.get(pk=old_ars_id) - def test_alternate_record_set_is_deleted_if_deleting_record_and_only_one_record_left(self): + def test_alternate_record_set_is_deleted_if_deleting_record_and_only_one_record_left( + self, + ): """ Same princible as above, but through deleting a record, instead of updating a record. @@ -1913,14 +2483,18 @@ def test_alternate_record_set_is_deleted_if_deleting_record_and_only_one_record_ self._set_preferred_identifier_to_record(pk=2, data_catalog=2) old_ars_id = CatalogRecord.objects.get(pk=2).alternate_record_set.id - response = self.client.delete('/rest/datasets/2', format="json") + response = self.client.delete("/rest/datasets/2", format="json") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) records = CatalogRecord.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}) - self.assertEqual(records.count(), 1, 'should be only record with this identifier left now') + research_dataset__contains={"preferred_identifier": self.preferred_identifier} + ) + self.assertEqual(records.count(), 1, "should be only record with this identifier left now") - with self.assertRaises(AlternateRecordSet.DoesNotExist, msg='alternate record set should have been deleted'): + with self.assertRaises( + AlternateRecordSet.DoesNotExist, + msg="alternate record set should have been deleted", + ): AlternateRecordSet.objects.get(pk=old_ars_id) def test_alternate_record_set_is_included_in_responses(self): @@ -1928,63 +2502,55 @@ def test_alternate_record_set_is_included_in_responses(self): Details of a dataset should contain field alternate_record_set in it. For a particular record, the set should not contain its own metadata_version_identifier in the set. """ - self.cr_test_data['data_catalog'] = 3 - msg_self_should_not_be_listed = 'identifier of the record itself should not be listed' + self.cr_test_data["data_catalog"] = 3 + msg_self_should_not_be_listed = "identifier of the record itself should not be listed" - response_1 = self.client.post('/rest/datasets', self.cr_test_data, format="json") - response_2 = self.client.get('/rest/datasets/1', format="json") + response_1 = self.client.post("/rest/datasets", self.cr_test_data, format="json") + response_2 = self.client.get("/rest/datasets/1", format="json") self.assertEqual(response_1.status_code, status.HTTP_201_CREATED) - self.assertEqual('alternate_record_set' in response_1.data, True) + self.assertEqual("alternate_record_set" in response_1.data, True) self.assertEqual( - response_1.data['identifier'] - not in response_1.data['alternate_record_set'], + response_1.data["identifier"] not in response_1.data["alternate_record_set"], True, - msg_self_should_not_be_listed + msg_self_should_not_be_listed, ) self.assertEqual( - response_2.data['identifier'] - in response_1.data['alternate_record_set'], - True + response_2.data["identifier"] in response_1.data["alternate_record_set"], + True, ) - self.cr_test_data.update({'data_catalog': 4}) - response_3 = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data.update({"data_catalog": 4}) + response_3 = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response_3.status_code, status.HTTP_201_CREATED) - self.assertEqual('alternate_record_set' in response_3.data, True) + self.assertEqual("alternate_record_set" in response_3.data, True) self.assertEqual( - response_1.data['identifier'] - in response_3.data['alternate_record_set'], - True + response_1.data["identifier"] in response_3.data["alternate_record_set"], + True, ) self.assertEqual( - response_2.data['identifier'] - in response_3.data['alternate_record_set'], - True + response_2.data["identifier"] in response_3.data["alternate_record_set"], + True, ) self.assertEqual( - response_3.data['identifier'] - not in response_3.data['alternate_record_set'], + response_3.data["identifier"] not in response_3.data["alternate_record_set"], True, - msg_self_should_not_be_listed + msg_self_should_not_be_listed, ) - response_2 = self.client.get('/rest/datasets/1', format="json") - self.assertEqual('alternate_record_set' in response_2.data, True) + response_2 = self.client.get("/rest/datasets/1", format="json") + self.assertEqual("alternate_record_set" in response_2.data, True) self.assertEqual( - response_1.data['identifier'] - in response_2.data['alternate_record_set'], - True + response_1.data["identifier"] in response_2.data["alternate_record_set"], + True, ) self.assertEqual( - response_3.data['identifier'] - in response_2.data['alternate_record_set'], - True + response_3.data["identifier"] in response_2.data["alternate_record_set"], + True, ) self.assertEqual( - response_2.data['identifier'] - not in response_2.data['alternate_record_set'], + response_2.data["identifier"] not in response_2.data["alternate_record_set"], True, - msg_self_should_not_be_listed + msg_self_should_not_be_listed, ) def _set_preferred_identifier_to_record(self, pk=1, data_catalog=1): @@ -1995,9 +2561,9 @@ def _set_preferred_identifier_to_record(self, pk=1, data_catalog=1): Note that if calling this method several times, this will also create an alternate_record_set (by calling _handle_preferred_identifier_changed()). """ - unique_identifier = 'im unique yo' + unique_identifier = "im unique yo" cr = CatalogRecord.objects.get(pk=pk) - cr.research_dataset['preferred_identifier'] = unique_identifier + cr.research_dataset["preferred_identifier"] = unique_identifier cr.data_catalog_id = data_catalog cr.force_save() cr._handle_preferred_identifier_changed() @@ -2015,9 +2581,14 @@ def _set_and_ensure_initial_conditions(self): # ensuring initial conditions... records = CatalogRecord.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}) - self.assertEqual(len(records), 3, - 'in the beginning, there should be three records with pref id %s' % self.preferred_identifier) + research_dataset__contains={"preferred_identifier": self.preferred_identifier} + ) + self.assertEqual( + len(records), + 3, + "in the beginning, there should be three records with pref id %s" + % self.preferred_identifier, + ) ars_id = records[0].alternate_record_set.id self.assertEqual(records[0].alternate_record_set.id, ars_id) self.assertEqual(records[1].alternate_record_set.id, ars_id) @@ -2038,55 +2609,61 @@ def test_update_from_0_to_n_files_does_not_create_new_version(self): The FIRST update from 0 to n files in a dataset should be permitted without creating a new dataset version. """ - data = self.client.get('/rest/datasets/1', format="json").data - data.pop('id') - data.pop('identifier') - data['research_dataset'].pop('preferred_identifier', None) - files = data['research_dataset'].pop('files', None) - data['research_dataset'].pop('directories', None) + data = self.client.get("/rest/datasets/1", format="json").data + data.pop("id") + data.pop("identifier") + data["research_dataset"].pop("preferred_identifier", None) + files = data["research_dataset"].pop("files", None) + data["research_dataset"].pop("directories", None) self.assertEqual(isinstance(files, list), True) # create test record - response = self.client.post('/rest/datasets', data, format="json") + response = self.client.post("/rest/datasets", data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # modify a few times to create metadata versions data = response.data - data['research_dataset']['title']['en'] = 'updated' - data = self.client.put('/rest/datasets/%d' % data['id'], data, format="json").data - data['research_dataset']['title']['en'] = 'updated again' - data = self.client.put('/rest/datasets/%d' % data['id'], data, format="json").data + data["research_dataset"]["title"]["en"] = "updated" + data = self.client.put("/rest/datasets/%d" % data["id"], data, format="json").data + data["research_dataset"]["title"]["en"] = "updated again" + data = self.client.put("/rest/datasets/%d" % data["id"], data, format="json").data - response = self.client.get('/rest/datasets', format="json") - dataset_count_beginning = response.data['count'] + response = self.client.get("/rest/datasets", format="json") + dataset_count_beginning = response.data["count"] # add files for the first time - should not create a new dataset version - data['research_dataset']['files'] = files - response = self.client.put('/rest/datasets/%d' % data['id'], data, format="json") + data["research_dataset"]["files"] = files + response = self.client.put("/rest/datasets/%d" % data["id"], data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('new_version_created' in response.data, False) + self.assertEqual("new_version_created" in response.data, False) # ensure no "ghost datasets" are created as residue - response = self.client.get('/rest/datasets', format="json") - self.assertEqual(response.data['count'], dataset_count_beginning, 'no new datasets should be created') + response = self.client.get("/rest/datasets", format="json") + self.assertEqual( + response.data["count"], + dataset_count_beginning, + "no new datasets should be created", + ) # remove files again... a new version is created normally - files = data['research_dataset'].pop('files') - response = self.client.put('/rest/datasets/%d' % data['id'], data, format="json") + files = data["research_dataset"].pop("files") + response = self.client.put("/rest/datasets/%d" % data["id"], data, format="json") new_version = self.get_next_version(response.data) - response = self.client.get('/rest/datasets', format="json") - self.assertEqual(response.data['count'], dataset_count_beginning + 1) + response = self.client.get("/rest/datasets", format="json") + self.assertEqual(response.data["count"], dataset_count_beginning + 1) # ...and put the files back. this is another 0->n files update. this time # should normally create new dataset version. - new_version['research_dataset']['files'] = files - response = self.client.put('/rest/datasets/%d' % new_version['id'], new_version, format="json") + new_version["research_dataset"]["files"] = files + response = self.client.put( + "/rest/datasets/%d" % new_version["id"], new_version, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('new_version_created' in response.data, True) + self.assertEqual("new_version_created" in response.data, True) - response = self.client.get('/rest/datasets', format="json") - self.assertEqual(response.data['count'], dataset_count_beginning + 2) + response = self.client.get("/rest/datasets", format="json") + self.assertEqual(response.data["count"], dataset_count_beginning + 2) def test_update_to_non_versioning_catalog_does_not_create_version(self): self._set_cr_to_catalog(pk=self.pk, dc=3) @@ -2094,18 +2671,22 @@ def test_update_to_non_versioning_catalog_does_not_create_version(self): self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_metadata_version_count(response.data, 0) - def test_update_to_versioning_catalog_with_preserve_version_parameter_does_not_create_version(self): + def test_update_to_versioning_catalog_with_preserve_version_parameter_does_not_create_version( + self, + ): self._set_cr_to_catalog(pk=self.pk, dc=1) - response = self._get_and_update_title(self.pk, params='?preserve_version') + response = self._get_and_update_title(self.pk, params="?preserve_version") self._assert_metadata_version_count(response.data, 0) def test_preserve_version_parameter_does_not_allow_file_changes(self): self._set_cr_to_catalog(pk=self.pk, dc=1) - data = self.client.get('/rest/datasets/%d' % self.pk, format="json").data - data['research_dataset']['files'][0]['identifier'] = 'pid:urn:11' - response = self.client.put('/rest/datasets/%d?preserve_version' % self.pk, data, format="json") + data = self.client.get("/rest/datasets/%d" % self.pk, format="json").data + data["research_dataset"]["files"][0]["identifier"] = "pid:urn:11" + response = self.client.put( + "/rest/datasets/%d?preserve_version" % self.pk, data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('not supported' in response.data['detail'][0], True, response.data) + self.assertEqual("not supported" in response.data["detail"][0], True, response.data) def test_update_rd_title_creates_new_metadata_version(self): """ @@ -2116,85 +2697,113 @@ def test_update_rd_title_creates_new_metadata_version(self): self._assert_metadata_version_count(response_1.data, 2) # get list of metadata versions to access contents... - response = self.client.get('/rest/datasets/%d/metadata_versions' % response_1.data['id'], format="json") + response = self.client.get( + "/rest/datasets/%d/metadata_versions" % response_1.data["id"], format="json" + ) - response_2 = self.client.get('/rest/datasets/%d/metadata_versions/%s' % - (self.pk, response.data[0]['metadata_version_identifier']), format="json") + response_2 = self.client.get( + "/rest/datasets/%d/metadata_versions/%s" + % (self.pk, response.data[0]["metadata_version_identifier"]), + format="json", + ) self.assertEqual(response_2.status_code, status.HTTP_200_OK, response_2.data) - self.assertEqual('preferred_identifier' in response_2.data, True) + self.assertEqual("preferred_identifier" in response_2.data, True) # note! response_1 == cr, response_2 == rd - self.assertEqual(response_1.data['research_dataset']['preferred_identifier'], - response_2.data['preferred_identifier']) + self.assertEqual( + response_1.data["research_dataset"]["preferred_identifier"], + response_2.data["preferred_identifier"], + ) def test_changing_files_creates_new_dataset_version(self): - cr = self.client.get('/rest/datasets/1').data - cr['research_dataset']['files'].pop(0) - response = self.client.put('/rest/datasets/1', cr, format="json") - self.assertEqual('next_dataset_version' in response.data, True) - self.assertEqual('new_version_created' in response.data, True) - self.assertEqual('dataset_version_set' in response.data, True) + cr = self.client.get("/rest/datasets/1").data + cr["research_dataset"]["files"].pop(0) + response = self.client.put("/rest/datasets/1", cr, format="json") + self.assertEqual("next_dataset_version" in response.data, True) + self.assertEqual("new_version_created" in response.data, True) + self.assertEqual("dataset_version_set" in response.data, True) def test_dataset_version_lists_removed_records(self): # create new version - cr = self.client.get('/rest/datasets/1').data - cr['research_dataset']['files'].pop(0) - response = self.client.put('/rest/datasets/1', cr, format="json") + cr = self.client.get("/rest/datasets/1").data + cr["research_dataset"]["files"].pop(0) + response = self.client.put("/rest/datasets/1", cr, format="json") # delete the new version - new_ver = response.data['next_dataset_version'] - response = self.client.delete('/rest/datasets/%d' % new_ver['id'], format="json") + new_ver = response.data["next_dataset_version"] + response = self.client.delete("/rest/datasets/%d" % new_ver["id"], format="json") # check deleted record is listed - response = self.client.get('/rest/datasets/1', format="json") - self.assertEqual(response.data['dataset_version_set'][0].get('removed', None), True, - response.data['dataset_version_set']) + response = self.client.get("/rest/datasets/1", format="json") + self.assertEqual( + response.data["dataset_version_set"][0].get("removed", None), + True, + response.data["dataset_version_set"], + ) def test_dataset_version_lists_date_removed(self): # get catalog record - cr = self.client.get('/rest/datasets/1').data + cr = self.client.get("/rest/datasets/1").data # create version2 - cr['research_dataset']['files'].pop(0) - response = self.client.put('/rest/datasets/1', cr, format="json") + cr["research_dataset"]["files"].pop(0) + response = self.client.put("/rest/datasets/1", cr, format="json") # delete version2 - version2 = response.data['next_dataset_version'] - response = self.client.delete('/rest/datasets/%d' % version2['id'], format="json") + version2 = response.data["next_dataset_version"] + response = self.client.delete("/rest/datasets/%d" % version2["id"], format="json") # check date_removed is listed and not None in deleted version - response = self.client.get('/rest/datasets/1', format="json") + response = self.client.get("/rest/datasets/1", format="json") - self.assertTrue(response.data['dataset_version_set'][0].get('date_removed')) - self.assertTrue(response.data['dataset_version_set'][0].get('date_removed') is not None) - self.assertFalse(response.data['dataset_version_set'][1].get('date_removed')) + self.assertTrue(response.data["dataset_version_set"][0].get("date_removed")) + self.assertTrue(response.data["dataset_version_set"][0].get("date_removed") is not None) + self.assertFalse(response.data["dataset_version_set"][1].get("date_removed")) - def test_new_dataset_version_pref_id_type_stays_same_as_previous_dataset_version_pref_id_type(self): + def test_new_dataset_version_pref_id_type_stays_same_as_previous_dataset_version_pref_id_type( + self, + ): # Create ida data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) + dc = self._get_object_from_test_data("datacatalog", requested_index=0) dc_id = IDA_CATALOG - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/datacatalogs', dc, format="json") + dc["catalog_json"]["identifier"] = dc_id + self.client.post("/rest/datacatalogs", dc, format="json") - self.cr_test_data['data_catalog'] = IDA_CATALOG - self.cr_test_data['research_dataset']['preferred_identifier'] = '' + self.cr_test_data["data_catalog"] = IDA_CATALOG + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" - cr_v1 = self.client.post('/rest/datasets?pid_type=urn', self.cr_test_data, format="json").data - cr_v1['research_dataset']['files'].pop(0) - cr_v2 = self.client.put('/rest/datasets/{0}?pid_type=doi'.format(cr_v1['identifier']), cr_v1, format="json") + cr_v1 = self.client.post( + "/rest/datasets?pid_type=urn", self.cr_test_data, format="json" + ).data + cr_v1["research_dataset"]["files"].pop(0) + cr_v2 = self.client.put( + "/rest/datasets/{0}?pid_type=doi".format(cr_v1["identifier"]), + cr_v1, + format="json", + ) self.assertEqual(cr_v2.status_code, status.HTTP_200_OK, cr_v2.data) - self.assertEqual('new_version_created' in cr_v2.data, True) + self.assertEqual("new_version_created" in cr_v2.data, True) self.assertTrue( - get_identifier_type(cr_v2.data['new_version_created']['preferred_identifier']) == IdentifierType.URN) + get_identifier_type(cr_v2.data["new_version_created"]["preferred_identifier"]) + == IdentifierType.URN + ) - cr_v1 = self.client.post('/rest/datasets?pid_type=doi', self.cr_test_data, format="json").data - cr_v1['research_dataset']['files'].pop(0) - cr_v2 = self.client.put('/rest/datasets/{0}'.format(cr_v1['identifier']), cr_v1, format="json") - self.assertEqual('new_version_created' in cr_v2.data, True) + cr_v1 = self.client.post( + "/rest/datasets?pid_type=doi", self.cr_test_data, format="json" + ).data + cr_v1["research_dataset"]["files"].pop(0) + cr_v2 = self.client.put( + "/rest/datasets/{0}".format(cr_v1["identifier"]), cr_v1, format="json" + ) + self.assertEqual("new_version_created" in cr_v2.data, True) self.assertTrue( - get_identifier_type(cr_v2.data['new_version_created']['preferred_identifier']) == IdentifierType.DOI) + get_identifier_type(cr_v2.data["new_version_created"]["preferred_identifier"]) + == IdentifierType.DOI + ) def _assert_metadata_version_count(self, record, count): - response = self.client.get('/rest/datasets/%d/metadata_versions' % record['id'], format="json") + response = self.client.get( + "/rest/datasets/%d/metadata_versions" % record["id"], format="json" + ) self.assertEqual(len(response.data), count) def _set_cr_to_catalog(self, pk=None, dc=None): @@ -2209,9 +2818,9 @@ def _get_and_update_title(self, pk, params=None): Should not force preferred_identifier to change. """ - data = self.client.get('/rest/datasets/%d' % pk, format="json").data - data['research_dataset']['title']['en'] = 'modified title' - return self.client.put('/rest/datasets/%d%s' % (pk, params or ''), data, format="json") + data = self.client.get("/rest/datasets/%d" % pk, format="json").data + data["research_dataset"]["title"]["en"] = "modified title" + return self.client.put("/rest/datasets/%d%s" % (pk, params or ""), data, format="json") def _get_and_update_files(self, pk, update_preferred_identifier=False, params=None): """ @@ -2222,70 +2831,74 @@ def _get_and_update_files(self, pk, update_preferred_identifier=False, params=No """ file_identifiers = [ { - 'identifier': f.identifier, - 'title': 'title', - 'use_category': { 'identifier': 'outcome' } + "identifier": f.identifier, + "title": "title", + "use_category": {"identifier": "outcome"}, } for f in File.objects.all() ] - data = self.client.get('/rest/datasets/%d' % pk, format="json").data - data['research_dataset']['files'] = file_identifiers[-5:] + data = self.client.get("/rest/datasets/%d" % pk, format="json").data + data["research_dataset"]["files"] = file_identifiers[-5:] if update_preferred_identifier: - new_pref_id = 'modified-preferred-identifier' - data['research_dataset']['preferred_identifier'] = new_pref_id + new_pref_id = "modified-preferred-identifier" + data["research_dataset"]["preferred_identifier"] = new_pref_id return ( - self.client.put('/rest/datasets/%d%s' % (pk, params or ''), data, format="json"), - new_pref_id + self.client.put("/rest/datasets/%d%s" % (pk, params or ""), data, format="json"), + new_pref_id, ) - return self.client.put('/rest/datasets/%d%s' % (pk, params or ''), data, format="json") + return self.client.put("/rest/datasets/%d%s" % (pk, params or ""), data, format="json") def test_allow_metadata_changes_after_deprecation(self): """ For deprecated datasets, file and directory additions/removals are forbidden but metadata changes are allowed. """ - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr = response.data - response = self.client.delete('/rest/files/1') + response = self.client.delete("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # after the dataset is deprecated, metadata updates should be ok - cr['research_dataset']['description'] = { + cr["research_dataset"]["description"] = { "en": "Updating new description for deprecated dataset should not create any problems" } - cr['research_dataset']['files'][0]['title'] = 'Brand new title 1' + cr["research_dataset"]["files"][0]["title"] = "Brand new title 1" - response = self.client.put('/rest/datasets/%s' % cr['id'], cr, format="json") + response = self.client.put("/rest/datasets/%s" % cr["id"], cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue('new description' in response.data['research_dataset']['description']['en'], - 'description field should be updated') - self.assertTrue('Brand new' in response.data['research_dataset']['files'][0]['title'], - 'title field for file should be updated') + self.assertTrue( + "new description" in response.data["research_dataset"]["description"]["en"], + "description field should be updated", + ) + self.assertTrue( + "Brand new" in response.data["research_dataset"]["files"][0]["title"], + "title field for file should be updated", + ) def test_prevent_adding_removed_file_to_deprecated_dataset(self): - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr = response.data # deprecates the dataset - response = self.client.delete('/rest/files/1') + response = self.client.delete("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # the file to add to data - response = self.client.delete('/rest/files/4') + response = self.client.delete("/rest/files/4") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - cr['research_dataset']['files'].append({ - "identifier": "pid:urn:4", - "title": "File Title", - "use_category": { - "identifier": "method" + cr["research_dataset"]["files"].append( + { + "identifier": "pid:urn:4", + "title": "File Title", + "use_category": {"identifier": "method"}, } - }) - response = self.client.put('/rest/datasets/%s' % cr['id'], cr, format="json") + ) + response = self.client.put("/rest/datasets/%s" % cr["id"], cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) @@ -2295,19 +2908,21 @@ class CatalogRecordApiWriteAssignFilesCommon(CatalogRecordApiWriteCommon): """ def _get_file_from_test_data(self): - from_test_data = self._get_object_from_test_data('file', requested_index=0) - from_test_data.update({ - "checksum": { - "value": "checksumvalue", - "algorithm": "SHA-256", - "checked": "2017-05-23T10:07:22.559656Z", - }, - "file_name": "must_replace", - "file_path": "must_replace", - "identifier": "must_replace", - "project_identifier": "must_replace", - "file_storage": self._get_object_from_test_data('filestorage', requested_index=0) - }) + from_test_data = self._get_object_from_test_data("file", requested_index=0) + from_test_data.update( + { + "checksum": { + "value": "checksumvalue", + "algorithm": "SHA-256", + "checked": "2017-05-23T10:07:22.559656Z", + }, + "file_name": "must_replace", + "file_path": "must_replace", + "identifier": "must_replace", + "project_identifier": "must_replace", + "file_storage": self._get_object_from_test_data("filestorage", requested_index=0), + } + ) return from_test_data def _form_test_file_hierarchy(self): @@ -2319,72 +2934,72 @@ def _form_test_file_hierarchy(self): { "file_name": "file_01.txt", "file_path": "/TestExperiment/Directory_1/Group_1/file_01.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_02.txt", "file_path": "/TestExperiment/Directory_1/Group_1/file_02.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_03.txt", "file_path": "/TestExperiment/Directory_1/Group_2/file_03.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_04.txt", "file_path": "/TestExperiment/Directory_1/Group_2/file_04.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_05.txt", "file_path": "/TestExperiment/Directory_1/file_05.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_06.txt", "file_path": "/TestExperiment/Directory_1/file_06.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_07.txt", "file_path": "/TestExperiment/Directory_2/Group_1/file_07.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_08.txt", "file_path": "/TestExperiment/Directory_2/Group_1/file_08.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_09.txt", "file_path": "/TestExperiment/Directory_2/Group_2/file_09.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_10.txt", "file_path": "/TestExperiment/Directory_2/Group_2/file_10.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_11.txt", "file_path": "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_12.txt", "file_path": "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_12.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_13.txt", "file_path": "/TestExperiment/Directory_2/file_13.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_14.txt", "file_path": "/TestExperiment/Directory_2/file_14.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, ] @@ -2392,176 +3007,175 @@ def _form_test_file_hierarchy(self): { "file_name": "file_15.txt", "file_path": "/SecondExperiment/Directory_1/Group_1/file_15.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_16.txt", "file_path": "/SecondExperiment/Directory_1/Group_1/file_16.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_17.txt", "file_path": "/SecondExperiment/Directory_1/Group_2/file_18.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_18.txt", "file_path": "/SecondExperiment/Directory_1/Group_2/file_18.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_19.txt", "file_path": "/SecondExperiment/Directory_1/file_19.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_20.txt", "file_path": "/SecondExperiment/Directory_1/file_20.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_21.txt", "file_path": "/SecondExperiment/Data/file_21.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_22.txt", "file_path": "/SecondExperiment/Data_Config/file_22.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_23.txt", "file_path": "/SecondExperiment/Data_Config/file_23.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_24.txt", "file_path": "/SecondExperiment/Data/History/file_24.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, - ] file_template = self._get_file_from_test_data() - del file_template['id'] - self._single_file_byte_size = file_template['byte_size'] + del file_template["id"] + self._single_file_byte_size = file_template["byte_size"] files_1 = [] for i, f in enumerate(file_data_1): file = deepcopy(file_template) - file.update(f, identifier='test:file:%s' % f['file_name'][-6:-4]) + file.update(f, identifier="test:file:%s" % f["file_name"][-6:-4]) files_1.append(file) files_2 = [] for i, f in enumerate(file_data_2): file = deepcopy(file_template) - file.update(f, identifier='test:file:%s' % f['file_name'][-6:-4]) + file.update(f, identifier="test:file:%s" % f["file_name"][-6:-4]) files_2.append(file) return files_1, files_2 def _add_directory(self, ds, path, project=None): - params = { 'directory_path': path } + params = {"directory_path": path} if project: - params['project_identifier'] = project + params["project_identifier"] = project identifier = Directory.objects.get(**params).identifier - if 'directories' not in ds['research_dataset']: - ds['research_dataset']['directories'] = [] + if "directories" not in ds["research_dataset"]: + ds["research_dataset"]["directories"] = [] - ds['research_dataset']['directories'].append({ - "identifier": identifier, - "title": "Directory Title", - "description": "This is directory at %s" % path, - "use_category": { - "identifier": "method" + ds["research_dataset"]["directories"].append( + { + "identifier": identifier, + "title": "Directory Title", + "description": "This is directory at %s" % path, + "use_category": {"identifier": "method"}, } - }) + ) def _add_file(self, ds, path): identifier = File.objects.filter(file_path__startswith=path).first().identifier - if 'files' not in ds['research_dataset']: - ds['research_dataset']['files'] = [] + if "files" not in ds["research_dataset"]: + ds["research_dataset"]["files"] = [] - ds['research_dataset']['files'].append({ - "identifier": identifier, - "title": "File Title", - "description": "This is file at %s" % path, - "use_category": { - "identifier": "method" + ds["research_dataset"]["files"].append( + { + "identifier": identifier, + "title": "File Title", + "description": "This is file at %s" % path, + "use_category": {"identifier": "method"}, } - }) + ) def _add_nonexisting_directory(self, ds): - ds['research_dataset']['directories'] = [{ - "identifier": "doesnotexist", - "title": "Directory Title", - "description": "This is directory does not exist", - "use_category": { - "identifier": "method" + ds["research_dataset"]["directories"] = [ + { + "identifier": "doesnotexist", + "title": "Directory Title", + "description": "This is directory does not exist", + "use_category": {"identifier": "method"}, } - }] + ] def _add_nonexisting_file(self, ds): - ds['research_dataset']['files'] = [{ - "identifier": "doesnotexist", - "title": "File Title", - "description": "This is file does not exist", - "use_category": { - "identifier": "method" + ds["research_dataset"]["files"] = [ + { + "identifier": "doesnotexist", + "title": "File Title", + "description": "This is file does not exist", + "use_category": {"identifier": "method"}, } - }] + ] def _remove_directory(self, ds, path): - if 'directories' not in ds['research_dataset']: - raise Exception('ds has no dirs') + if "directories" not in ds["research_dataset"]: + raise Exception("ds has no dirs") identifier = Directory.objects.get(directory_path=path).identifier - for i, dr in enumerate(ds['research_dataset']['directories']): - if dr['identifier'] == identifier: - ds['research_dataset']['directories'].pop(i) + for i, dr in enumerate(ds["research_dataset"]["directories"]): + if dr["identifier"] == identifier: + ds["research_dataset"]["directories"].pop(i) return - raise Exception('path %s not found in directories' % path) + raise Exception("path %s not found in directories" % path) def _remove_file(self, ds, path): - if 'files' not in ds['research_dataset']: - raise Exception('ds has no files') + if "files" not in ds["research_dataset"]: + raise Exception("ds has no files") identifier = File.objects.get(file_path=path).identifier - for i, f in enumerate(ds['research_dataset']['files']): - if f['identifier'] == identifier: - ds['research_dataset']['files'].pop(i) + for i, f in enumerate(ds["research_dataset"]["files"]): + if f["identifier"] == identifier: + ds["research_dataset"]["files"].pop(i) return - raise Exception('path %s not found in files' % path) + raise Exception("path %s not found in files" % path) def _freeze_new_files(self): file_data = [ { "file_name": "file_90.txt", "file_path": "/TestExperiment/Directory_2/Group_3/file_90.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_91.txt", "file_path": "/TestExperiment/Directory_2/Group_3/file_91.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, ] file_template = self._get_file_from_test_data() - del file_template['id'] - self._single_file_byte_size = file_template['byte_size'] + del file_template["id"] + self._single_file_byte_size = file_template["byte_size"] files = [] for i, f in enumerate(file_data): file = deepcopy(file_template) - file.update(f, identifier='frozen:later:file:%s' % f['file_name'][-6:-4]) + file.update(f, identifier="frozen:later:file:%s" % f["file_name"][-6:-4]) files.append(file) - response = self.client.post('/rest/files', files, format="json") + response = self.client.post("/rest/files", files, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) def _freeze_files_to_root(self): @@ -2569,25 +3183,25 @@ def _freeze_files_to_root(self): { "file_name": "file_56.txt", "file_path": "/TestExperiment/Directory_2/file_56.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_57.txt", "file_path": "/TestExperiment/Directory_2/file_57.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, ] file_template = self._get_file_from_test_data() - del file_template['id'] - self._single_file_byte_size = file_template['byte_size'] + del file_template["id"] + self._single_file_byte_size = file_template["byte_size"] files = [] for i, f in enumerate(file_data): file = deepcopy(file_template) - file.update(f, identifier='frozen:later:file:%s' % f['file_name'][-6:-4]) + file.update(f, identifier="frozen:later:file:%s" % f["file_name"][-6:-4]) files.append(file) - response = self.client.post('/rest/files', files, format="json") + response = self.client.post("/rest/files", files, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) def setUp(self): @@ -2597,28 +3211,34 @@ def setUp(self): - create 12 new files in a new project """ super().setUp() - self.cr_test_data['research_dataset'].pop('id', None) - self.cr_test_data['research_dataset'].pop('preferred_identifier', None) - self.cr_test_data['research_dataset'].pop('files', None) - self.cr_test_data['research_dataset'].pop('directories', None) + self.cr_test_data["research_dataset"].pop("id", None) + self.cr_test_data["research_dataset"].pop("preferred_identifier", None) + self.cr_test_data["research_dataset"].pop("files", None) + self.cr_test_data["research_dataset"].pop("directories", None) project_files = self._form_test_file_hierarchy() for p_files in project_files: - response = self.client.post('/rest/files', p_files, format="json") + response = self.client.post("/rest/files", p_files, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def assert_preferred_identifier_changed(self, response, true_or_false): self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('next_dataset_version' in response.data, true_or_false, - 'this field should only be present if preferred_identifier changed') + self.assertEqual( + "next_dataset_version" in response.data, + true_or_false, + "this field should only be present if preferred_identifier changed", + ) if true_or_false is True: - self.assertEqual(response.data['research_dataset']['preferred_identifier'] != - response.data['next_dataset_version']['preferred_identifier'], true_or_false) + self.assertEqual( + response.data["research_dataset"]["preferred_identifier"] + != response.data["next_dataset_version"]["preferred_identifier"], + true_or_false, + ) def assert_file_count(self, cr, expected_file_count): - self.assertEqual(CatalogRecord.objects.get(pk=cr['id']).files.count(), expected_file_count) + self.assertEqual(CatalogRecord.objects.get(pk=cr["id"]).files.count(), expected_file_count) def assert_total_files_byte_size(self, cr, expected_size): - self.assertEqual(cr['research_dataset']['total_files_byte_size'], expected_size) + self.assertEqual(cr["research_dataset"]["total_files_byte_size"], expected_size) class CatalogRecordApiWriteCumulativeDatasets(CatalogRecordApiWriteAssignFilesCommon): @@ -2633,98 +3253,136 @@ def _create_cumulative_dataset_with_files(self): """ Create cumulative dataset with two files that will be updated. """ - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_05.txt') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_06.txt') - self.cr_test_data['cumulative_state'] = 1 # YES + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_05.txt") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_06.txt") + self.cr_test_data["cumulative_state"] = 1 # YES - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - return response.data # i.e. the dataset + return response.data # i.e. the dataset def _create_cumulative_dataset_without_files(self): """ Create cumulative dataset without any files. """ - self.cr_test_data['cumulative_state'] = 1 # YES + self.cr_test_data["cumulative_state"] = 1 # YES - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) return response.data def test_create_cumulative_dataset_with_state_closed(self): - self.cr_test_data['cumulative_state'] = 2 # CLOSED + self.cr_test_data["cumulative_state"] = 2 # CLOSED - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_create_cumulative_dataset_with_preservation_state(self): - self.cr_test_data['cumulative_state'] = 1 - self.cr_test_data['preservation_state'] = 10 + self.cr_test_data["cumulative_state"] = 1 + self.cr_test_data["preservation_state"] = 10 - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('PAS' in response.data['detail'][0], response.data) + self.assertTrue("PAS" in response.data["detail"][0], response.data) def test_create_cumulative_dataset_sets_date_cumulation_started(self): - self.cr_test_data['cumulative_state'] = 1 + self.cr_test_data["cumulative_state"] = 1 - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['date_cumulation_started'], response.data['date_created'], response.data) - self.assertTrue('date_cumulation_ended' not in response.data, response.data) - self.assertTrue('date_last_cumulative_addition' not in response.data, response.data) + self.assertEqual( + response.data["date_cumulation_started"], + response.data["date_created"], + response.data, + ) + self.assertTrue("date_cumulation_ended" not in response.data, response.data) + self.assertTrue("date_last_cumulative_addition" not in response.data, response.data) def test_add_files_to_empty_cumulative_dataset(self): cr = self._create_cumulative_dataset_without_files() total_record_count_beginning = CatalogRecord.objects_unfiltered.all().count() - self._add_file(cr, '/TestExperiment/Directory_1/Group_1/file_01.txt') + self._add_file(cr, "/TestExperiment/Directory_1/Group_1/file_01.txt") response = self.update_record(cr) current_record_count = CatalogRecord.objects_unfiltered.all().count() self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(current_record_count, total_record_count_beginning, 'there should be no new datasets') + self.assertEqual( + current_record_count, + total_record_count_beginning, + "there should be no new datasets", + ) + + def test_total_files_byte_size_field_is_dropped_from_datasets_with_no_files(self): + # dataset with no files/dirs does not have total_files_byte_size field + self.cr_test_data["research_dataset"].pop("files", None) + self.cr_test_data["research_dataset"].pop("directories", None) + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") + cr_id = response.data["id"] + response = self.client.get(f"/rest/datasets/{cr_id}") + self.assertEqual(response.data.get("research_dataset").get("total_files_byte_size"), None) def test_adding_files_to_cumulative_dataset_creates_no_new_versions(self): """ Tests the basic idea of cumulative dataset: add files with no new version """ cr = self._create_cumulative_dataset_with_files() - self._add_file(cr, '/TestExperiment/Directory_1/Group_1/file_01.txt') - self._add_file(cr, '/TestExperiment/Directory_1/Group_1/file_02.txt') + self._add_file(cr, "/TestExperiment/Directory_1/Group_1/file_01.txt") + self._add_file(cr, "/TestExperiment/Directory_1/Group_1/file_02.txt") total_record_count_beginning = CatalogRecord.objects_unfiltered.all().count() response = self.update_record(cr) current_record_count = CatalogRecord.objects_unfiltered.all().count() self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(current_record_count, total_record_count_beginning, 'there should be no new datasets') + self.assertEqual( + current_record_count, + total_record_count_beginning, + "there should be no new datasets", + ) # two + two is four, quik mafs self.assert_file_count(response.data, 4) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 4) - self.assertEqual('new_dataset_version' in response.data, False, 'New version should not be created') + self.assertEqual( + "new_dataset_version" in response.data, + False, + "New version should not be created", + ) cr = response.data - self._add_directory(cr, '/TestExperiment/Directory_2/Group_2') + self._add_directory(cr, "/TestExperiment/Directory_2/Group_2") response = self.update_record(cr) current_record_count = CatalogRecord.objects_unfiltered.all().count() self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(current_record_count, total_record_count_beginning, 'there should be no new datasets') + self.assertEqual( + current_record_count, + total_record_count_beginning, + "there should be no new datasets", + ) self.assert_file_count(response.data, 8) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 8) - self.assertEqual('new_dataset_version' in response.data, False, 'New version should not be created') + self.assertEqual( + "new_dataset_version" in response.data, + False, + "New version should not be created", + ) - def test_adding_files_to_cumulative_dataset_changes_date_last_cumulative_addition(self): + def test_adding_files_to_cumulative_dataset_changes_date_last_cumulative_addition( + self, + ): cr = self._create_cumulative_dataset_with_files() - self._add_file(cr, '/TestExperiment/Directory_1/Group_1/file_01.txt') - self._add_file(cr, '/TestExperiment/Directory_1/Group_1/file_02.txt') - sleep(1) # ensure that next request happens with different timestamp + self._add_file(cr, "/TestExperiment/Directory_1/Group_1/file_01.txt") + self._add_file(cr, "/TestExperiment/Directory_1/Group_1/file_02.txt") + sleep(1) # ensure that next request happens with different timestamp response = self.update_record(cr) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data['date_last_cumulative_addition'] != response.data['date_created'], response.data) + self.assertTrue( + response.data["date_last_cumulative_addition"] != response.data["date_created"], + response.data, + ) def test_add_single_sub_directory(self): """ @@ -2732,14 +3390,14 @@ def test_add_single_sub_directory(self): """ cr = self._create_cumulative_dataset_with_files() - self._add_directory(cr, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') + self._add_directory(cr, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") response = self.update_record(cr) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assert_file_count(response.data, 4) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 4) cr = response.data - self._add_directory(response.data, '/TestExperiment/Directory_2/Group_2') + self._add_directory(response.data, "/TestExperiment/Directory_2/Group_2") response = self.update_record(cr) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assert_file_count(response.data, 6) @@ -2752,14 +3410,14 @@ def test_single_common_root_directory(self): """ cr = self._create_cumulative_dataset_with_files() - self._add_directory(cr, '/TestExperiment/Directory_2') + self._add_directory(cr, "/TestExperiment/Directory_2") response = self.update_record(cr) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assert_file_count(response.data, 10) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 10) - self._add_directory(cr, '/TestExperiment/Directory_2/Group_2') - self._add_directory(cr, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') + self._add_directory(cr, "/TestExperiment/Directory_2/Group_2") + self._add_directory(cr, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") response = self.update_record(cr) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assert_file_count(response.data, 10) @@ -2775,28 +3433,31 @@ def test_add_multiple_files_and_directories(self): cr = self._create_cumulative_dataset_with_files() # add one directory, which holds a two files and one sub directory which also holds two files. # four new files are added - self._add_directory(cr, '/TestExperiment/Directory_2/Group_2') + self._add_directory(cr, "/TestExperiment/Directory_2/Group_2") response = self.update_record(cr) self.assert_file_count(response.data, 6) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 6) # separately add (describe) a child directory of the previous dir. # no new files are added - self._add_directory(response.data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') + self._add_directory(response.data, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") response = self.update_record(response.data) self.assert_file_count(response.data, 6) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 6) # add a single new file not included by the previously added directories. # new files are added - self._add_file(response.data, '/TestExperiment/Directory_2/file_14.txt') + self._add_file(response.data, "/TestExperiment/Directory_2/file_14.txt") response = self.update_record(response.data) self.assert_file_count(response.data, 7) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 7) # add a single new file already included by the previously added directories. # new files are not added - self._add_file(response.data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt') + self._add_file( + response.data, + "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt", + ) response = self.update_record(response.data) self.assert_file_count(response.data, 7) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 7) @@ -2813,7 +3474,7 @@ def test_add_files_which_were_frozen_later(self): cr = self._create_cumulative_dataset_with_files() # add new root directory which holds eight files - self._add_directory(cr, '/TestExperiment/Directory_2') + self._add_directory(cr, "/TestExperiment/Directory_2") response = self.update_record(cr) self.assert_preferred_identifier_changed(response, False) self.assert_file_count(response.data, 10) @@ -2831,7 +3492,7 @@ def test_add_files_which_were_frozen_later(self): self._freeze_new_files() # only added files are included in the dataset - self._add_file(response.data, '/TestExperiment/Directory_2/Group_3/file_90.txt') + self._add_file(response.data, "/TestExperiment/Directory_2/Group_3/file_90.txt") response = self.update_record(response.data) self.assert_preferred_identifier_changed(response, False) self.assert_file_count(response.data, 11) @@ -2844,16 +3505,16 @@ def test_metadata_changes_do_not_add_later_frozen_files(self): specifically added in the update. """ # create the original record with just one directory - self.cr_test_data['cumulative_state'] = 1 - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["cumulative_state"] = 1 + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 8) original_version = response.data self._freeze_new_files() - original_version['research_dataset']['version_notes'] = [str(datetime.now())] + original_version["research_dataset"]["version_notes"] = [str(datetime.now())] response = self.update_record(original_version) self.assert_file_count(response.data, 8) @@ -2864,41 +3525,43 @@ def test_add_files_with_preserve_version_flag(self): to allow adding with same version. """ cr = self._create_cumulative_dataset_with_files() - self._add_file(cr, '/TestExperiment/Directory_1/Group_1/file_01.txt') - response = self.client.put('/rest/datasets/%s?preserve_version' % cr['identifier'], cr, format="json") + self._add_file(cr, "/TestExperiment/Directory_1/Group_1/file_01.txt") + response = self.client.put( + "/rest/datasets/%s?preserve_version" % cr["identifier"], cr, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) def test_change_preservation_state(self): cr = self._create_cumulative_dataset_with_files() - cr['preservation_state'] = 10 - response = self.client.put('/rest/datasets/%s' % cr['identifier'], cr, format="json") + cr["preservation_state"] = 10 + response = self.client.put("/rest/datasets/%s" % cr["identifier"], cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_remove_files_from_cumulative_dataset(self): cr = self._create_cumulative_dataset_with_files() - self._remove_file(cr, '/TestExperiment/Directory_1/file_06.txt') + self._remove_file(cr, "/TestExperiment/Directory_1/file_06.txt") response = self.update_record(cr) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # ensure that there are no changes - response = self.client.get('/rest/datasets/%s' % cr['identifier'], format="json") + response = self.client.get("/rest/datasets/%s" % cr["identifier"], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assert_file_count(response.data, 2) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 2) def test_remove_directory_from_cumulative_dataset(self): cr = self._create_cumulative_dataset_with_files() - self._add_directory(cr, '/TestExperiment/Directory_2') + self._add_directory(cr, "/TestExperiment/Directory_2") response = self.update_record(cr) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._remove_directory(cr, '/TestExperiment/Directory_2') + self._remove_directory(cr, "/TestExperiment/Directory_2") response = self.update_record(cr) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # ensure that there are no changes - response = self.client.get('/rest/datasets/%s' % cr['identifier'], format="json") + response = self.client.get("/rest/datasets/%s" % cr["identifier"], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assert_file_count(response.data, 10) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 10) @@ -2916,9 +3579,9 @@ def test_files_are_saved_during_create(self): """ A very simple "add two individual files" test. """ - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_05.txt') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_06.txt') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_05.txt") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_06.txt") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 2) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 2) @@ -2927,9 +3590,9 @@ def test_directories_are_saved_during_create(self): """ A very simple "add two individual directories" test. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_1') - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1/Group_1") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1/Group_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 4) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 4) @@ -2938,10 +3601,10 @@ def test_single_common_root_directory(self): """ A very simple "there is a single common root directory" test. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2') - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 8) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 8) @@ -2951,11 +3614,13 @@ def test_directory_names_are_similar(self): Ensure similar directory names are not mistaken to have parent/child relations, i.e. directory separator-character is the true separator of dirs in the path. """ - self._add_directory(self.cr_test_data, '/SecondExperiment/Data') - self._add_directory(self.cr_test_data, '/SecondExperiment/Data/History') - self._add_directory(self.cr_test_data, '/SecondExperiment/Data_Config') # the interesting dir + self._add_directory(self.cr_test_data, "/SecondExperiment/Data") + self._add_directory(self.cr_test_data, "/SecondExperiment/Data/History") + self._add_directory( + self.cr_test_data, "/SecondExperiment/Data_Config" + ) # the interesting dir - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 4) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 4) @@ -2964,11 +3629,11 @@ def test_files_and_directories_are_saved_during_create(self): """ A very simple "add two individual directories and two files" test. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_1') - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_2') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_05.txt') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_06.txt') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1/Group_1") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1/Group_2") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_05.txt") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_06.txt") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 6) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 6) @@ -2977,10 +3642,10 @@ def test_files_and_directories_are_saved_during_create_2(self): """ Save a directory, and also two files from the same directory. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_1') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/Group_1/file_01.txt') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/Group_1/file_02.txt') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1/Group_1") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/Group_1/file_01.txt") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/Group_1/file_02.txt") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 2) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 2) @@ -2990,16 +3655,16 @@ def test_empty_files_and_directories_arrays_are_removed(self): If an update is trying to leave empty "files" or "directories" array into research_dataset, they should be removed entirely during the update. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_1') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/Group_1/file_01.txt') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1/Group_1") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/Group_1/file_01.txt") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") cr = response.data - cr['research_dataset']['directories'] = [] - cr['research_dataset']['files'] = [] - response = self.client.put('/rest/datasets/%d' % cr['id'], cr, format="json") + cr["research_dataset"]["directories"] = [] + cr["research_dataset"]["files"] = [] + response = self.client.put("/rest/datasets/%d" % cr["id"], cr, format="json") new_version = self.get_next_version(response.data) - self.assertEqual('directories' in new_version['research_dataset'], False, response.data) - self.assertEqual('files' in new_version['research_dataset'], False, response.data) + self.assertEqual("directories" in new_version["research_dataset"], False, response.data) + self.assertEqual("files" in new_version["research_dataset"], False, response.data) def test_multiple_file_and_directory_changes(self): """ @@ -3011,15 +3676,15 @@ def test_multiple_file_and_directory_changes(self): """ # create the original record with just one file - self._add_file(self.cr_test_data, '/TestExperiment/Directory_2/file_13.txt') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_2/file_13.txt") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(CatalogRecord.objects.get(pk=response.data['id']).files.count(), 1) + self.assertEqual(CatalogRecord.objects.get(pk=response.data["id"]).files.count(), 1) # add one directory, which holds a couple of files and one sub directory which also holds two files. # new files are added original_version = response.data - self._add_directory(original_version, '/TestExperiment/Directory_2/Group_2') + self._add_directory(original_version, "/TestExperiment/Directory_2/Group_2") response = self.update_record(original_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3028,13 +3693,13 @@ def test_multiple_file_and_directory_changes(self): # separately add (describe) a child directory of the previous dir. # no new files are added - self._add_directory(new_version, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') + self._add_directory(new_version, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") response = self.update_record(new_version) - self.assertEqual('new_dataset_version' in response.data, False) + self.assertEqual("new_dataset_version" in response.data, False) # add a single new file not included by the previously added directories. # new files are added - self._add_file(new_version, '/TestExperiment/Directory_2/file_14.txt') + self._add_file(new_version, "/TestExperiment/Directory_2/file_14.txt") response = self.update_record(new_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3043,7 +3708,7 @@ def test_multiple_file_and_directory_changes(self): # remove the previously added file, not included by the previously added directories. # files are removed - self._remove_file(new_version, '/TestExperiment/Directory_2/file_14.txt') + self._remove_file(new_version, "/TestExperiment/Directory_2/file_14.txt") response = self.update_record(new_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3052,26 +3717,32 @@ def test_multiple_file_and_directory_changes(self): # add a single new file already included by the previously added directories. # new files are not added - self._add_file(new_version, '/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt') + self._add_file( + new_version, + "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt", + ) response = self.update_record(new_version) - self.assertEqual('new_dataset_version' in response.data, False) + self.assertEqual("new_dataset_version" in response.data, False) # remove the sub dir added previously. files are also still contained by the other upper dir. # files are not removed - self._remove_directory(new_version, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') + self._remove_directory(new_version, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") response = self.update_record(new_version) - self.assertEqual('new_dataset_version' in response.data, False) + self.assertEqual("new_dataset_version" in response.data, False) # remove a previously added file, the file is still contained by the other upper dir. # files are not removed - self._remove_file(new_version, '/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt') + self._remove_file( + new_version, + "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt", + ) response = self.update_record(new_version) - self.assertEqual('new_dataset_version' in response.data, False) + self.assertEqual("new_dataset_version" in response.data, False) # remove the last directory, which should remove the 4 files included by this dir and the sub dir. # files are removed. # only the originally added single file should be left. - self._remove_directory(new_version, '/TestExperiment/Directory_2/Group_2') + self._remove_directory(new_version, "/TestExperiment/Directory_2/Group_2") response = self.update_record(new_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3089,16 +3760,16 @@ def test_add_files_which_were_frozen_later(self): """ # create the original record with just one directory - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(CatalogRecord.objects.get(pk=response.data['id']).files.count(), 8) + self.assertEqual(CatalogRecord.objects.get(pk=response.data["id"]).files.count(), 8) original_version = response.data self._freeze_new_files() # add one new file - self._add_file(original_version, '/TestExperiment/Directory_2/Group_3/file_90.txt') + self._add_file(original_version, "/TestExperiment/Directory_2/Group_3/file_90.txt") response = self.update_record(original_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3106,7 +3777,7 @@ def test_add_files_which_were_frozen_later(self): self.assert_total_files_byte_size(new_version, self._single_file_byte_size * 9) # add one new directory, which holds one new file, since the other file was already added - self._add_directory(new_version, '/TestExperiment/Directory_2/Group_3') + self._add_directory(new_version, "/TestExperiment/Directory_2/Group_3") response = self.update_record(new_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3121,17 +3792,17 @@ def test_metadata_changes_do_not_add_later_frozen_files(self): """ # create the original record with just one directory - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(CatalogRecord.objects.get(pk=response.data['id']).files.count(), 8) + self.assertEqual(CatalogRecord.objects.get(pk=response.data["id"]).files.count(), 8) original_version = response.data self._freeze_new_files() - original_version['research_dataset']['version_notes'] = [str(datetime.now())] + original_version["research_dataset"]["version_notes"] = [str(datetime.now())] response = self.update_record(original_version) - self.assertEqual('next_dataset_version' in response.data, False) + self.assertEqual("next_dataset_version" in response.data, False) self.assert_file_count(response.data, 8) def test_removing_top_level_directory_does_not_remove_all_files(self): @@ -3144,11 +3815,13 @@ def test_removing_top_level_directory_does_not_remove_all_files(self): # note: see the method _form_test_file_hierarchy() to inspect what the directories # contain in more detail. - self._add_directory(self.cr_test_data, '/TestExperiment') # 14 files (removed later) - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1') # 6 files - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') # 8 files (removed later) - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2') # 4 files - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment") # 14 files (removed later) + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1") # 6 files + self._add_directory( + self.cr_test_data, "/TestExperiment/Directory_2" + ) # 8 files (removed later) + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2") # 4 files + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 14) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 14) @@ -3157,8 +3830,8 @@ def test_removing_top_level_directory_does_not_remove_all_files(self): # remove the root dir, and another sub-dir. there should be two directories left. both of them # are now "top-level directories", since they have no common parent. # files are removed - self._remove_directory(original_version, '/TestExperiment') - self._remove_directory(original_version, '/TestExperiment/Directory_2') + self._remove_directory(original_version, "/TestExperiment") + self._remove_directory(original_version, "/TestExperiment/Directory_2") response = self.update_record(original_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3169,8 +3842,8 @@ def test_add_multiple_directories(self): """ Ensure adding multiple directories at once really adds files from all new directories. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 2) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 2) @@ -3178,9 +3851,9 @@ def test_add_multiple_directories(self): # add new directories. # files are added - self._add_directory(original_version, '/TestExperiment/Directory_2/Group_1') - self._add_directory(original_version, '/TestExperiment/Directory_2/Group_2') - self._add_directory(original_version, '/SecondExperiment/Directory_1/Group_1') + self._add_directory(original_version, "/TestExperiment/Directory_2/Group_1") + self._add_directory(original_version, "/TestExperiment/Directory_2/Group_2") + self._add_directory(original_version, "/SecondExperiment/Directory_1/Group_1") response = self.update_record(original_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3191,10 +3864,10 @@ def test_add_multiple_directories_2(self): """ Ensure adding multiple directories at once really adds files from all new directories. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1') - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 14) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 14) @@ -3204,9 +3877,9 @@ def test_add_files_from_different_projects(self): Add directories from two different projects, ensure both projects' top-level dirs are handled properly, and none of the projects interferes with each other. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') - self._add_directory(self.cr_test_data, '/SecondExperiment/Directory_1/Group_1') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") + self._add_directory(self.cr_test_data, "/SecondExperiment/Directory_1/Group_1") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 4) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 4) @@ -3214,7 +3887,7 @@ def test_add_files_from_different_projects(self): # add a new directory. this is a top-level of the previous dir, which contains new files. # files are added - self._add_directory(original_version, '/TestExperiment/Directory_2/Group_2') + self._add_directory(original_version, "/TestExperiment/Directory_2/Group_2") response = self.update_record(original_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3223,7 +3896,7 @@ def test_add_files_from_different_projects(self): # remove the previously added dir, which is now the top-level dir in that project. # files are removed - self._remove_directory(new_version, '/TestExperiment/Directory_2/Group_2') + self._remove_directory(new_version, "/TestExperiment/Directory_2/Group_2") response = self.update_record(new_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3232,7 +3905,7 @@ def test_add_files_from_different_projects(self): # remove dirs from the second project entirely. # files are removed - self._remove_directory(new_version, '/SecondExperiment/Directory_1/Group_1') + self._remove_directory(new_version, "/SecondExperiment/Directory_1/Group_1") response = self.update_record(new_version) self.assert_preferred_identifier_changed(response, True) new_version = self.get_next_version(response.data) @@ -3240,16 +3913,16 @@ def test_add_files_from_different_projects(self): self.assert_total_files_byte_size(new_version, self._single_file_byte_size * 2) def test_file_not_found(self): - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") original_version = response.data self._add_nonexisting_file(original_version) response = self.update_record(original_version) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_directory_not_found(self): - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") original_version = response.data self._add_nonexisting_directory(original_version) response = self.update_record(original_version) @@ -3262,19 +3935,25 @@ def test_prevent_file_changes_to_old_dataset_versions(self): """ # create original record - self._add_file(self.cr_test_data, '/TestExperiment/Directory_2/file_13.txt') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_2/file_13.txt") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") original_version = response.data # make a file change, so that a new dataset version is created - self._add_file(original_version, '/TestExperiment/Directory_2/file_14.txt') + self._add_file(original_version, "/TestExperiment/Directory_2/file_14.txt") response = self.update_record(original_version) # now try to make a file change to the older dataset versions. this should not be permitted - self._add_file(original_version, '/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt') + self._add_file( + original_version, + "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt", + ) response = self.update_record(original_version) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, - 'file changes in old dataset versions should not be allowed') + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + "file changes in old dataset versions should not be allowed", + ) # other tests related to adding files / dirs @@ -3283,57 +3962,60 @@ def test_file_and_dir_titles_are_populated_when_omitted(self): If field 'title' is omitted from file or dir metadata, their respective file_name or directory_name should automatically be populated as title. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/Group_1/file_01.txt') + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/Group_1/file_01.txt") # ensure titles are not overwritten when specified by the user orig_titles = [ - self.cr_test_data['research_dataset']['files'][0]['title'], - self.cr_test_data['research_dataset']['directories'][0]['title'], + self.cr_test_data["research_dataset"]["files"][0]["title"], + self.cr_test_data["research_dataset"]["directories"][0]["title"], ] - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assertTrue( - response.data['research_dataset']['files'][0]['title'] in orig_titles, - response.data['research_dataset']['files'][0]['title'] + response.data["research_dataset"]["files"][0]["title"] in orig_titles, + response.data["research_dataset"]["files"][0]["title"], ) self.assertTrue( - response.data['research_dataset']['directories'][0]['title'] in orig_titles, - response.data['research_dataset']['directories'][0]['title'] + response.data["research_dataset"]["directories"][0]["title"] in orig_titles, + response.data["research_dataset"]["directories"][0]["title"], ) # ensure titles are automatically populated when omitted by the user - del self.cr_test_data['research_dataset']['files'][0]['title'] - del self.cr_test_data['research_dataset']['directories'][0]['title'] + del self.cr_test_data["research_dataset"]["files"][0]["title"] + del self.cr_test_data["research_dataset"]["directories"][0]["title"] - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assertTrue( - response.data['research_dataset']['files'][0]['title'] not in orig_titles, - response.data['research_dataset']['files'][0]['title'] + response.data["research_dataset"]["files"][0]["title"] not in orig_titles, + response.data["research_dataset"]["files"][0]["title"], ) self.assertTrue( - response.data['research_dataset']['directories'][0]['title'] not in orig_titles, - response.data['research_dataset']['directories'][0]['title'] + response.data["research_dataset"]["directories"][0]["title"] not in orig_titles, + response.data["research_dataset"]["directories"][0]["title"], ) def test_prevent_non_existent_additions_to_deprecated_dataset(self): - self._add_file(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_file( + self.cr_test_data, + "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt", + ) + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr = response.data - file_id = cr['research_dataset']['files'][0]['identifier'] + file_id = cr["research_dataset"]["files"][0]["identifier"] - response = self.client.delete('/rest/files/%s' % file_id, format="json") + response = self.client.delete("/rest/files/%s" % file_id, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._add_nonexisting_directory(cr) - response = self.client.put('/rest/datasets/%s' % cr['id'], cr, format="json") + response = self.client.put("/rest/datasets/%s" % cr["id"], cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) @@ -3344,15 +4026,20 @@ class CatalogRecordApiWriteRemoteResources(CatalogRecordApiWriteCommon): """ def test_calculate_total_remote_resources_byte_size(self): - cr_with_rr = self._get_object_from_test_data('catalogrecord', requested_index=14) - rr = cr_with_rr['research_dataset']['remote_resources'] - total_remote_resources_byte_size = sum(res['byte_size'] for res in rr) - self.cr_att_test_data['research_dataset']['remote_resources'] = rr - response = self.client.post('/rest/datasets', self.cr_att_test_data, format="json") + cr_with_rr = self._get_object_from_test_data("catalogrecord", requested_index=14) + rr = cr_with_rr["research_dataset"]["remote_resources"] + total_remote_resources_byte_size = sum(res["byte_size"] for res in rr) + self.cr_att_test_data["research_dataset"]["remote_resources"] = rr + response = self.client.post("/rest/datasets", self.cr_att_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('total_remote_resources_byte_size' in response.data['research_dataset'], True) - self.assertEqual(response.data['research_dataset']['total_remote_resources_byte_size'], - total_remote_resources_byte_size) + self.assertEqual( + "total_remote_resources_byte_size" in response.data["research_dataset"], + True, + ) + self.assertEqual( + response.data["research_dataset"]["total_remote_resources_byte_size"], + total_remote_resources_byte_size, + ) class CatalogRecordApiWriteLegacyDataCatalogs(CatalogRecordApiWriteCommon): @@ -3366,55 +4053,57 @@ def setUp(self): Create a test-datacatalog that plays the role of a legacy catalog. """ super().setUp() - dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema='att').first() - dc.catalog_json['identifier'] = LEGACY_CATALOGS[0] + dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema="att").first() + dc.catalog_json["identifier"] = LEGACY_CATALOGS[0] dc.force_save() - del self.cr_test_data['research_dataset']['files'] - del self.cr_test_data['research_dataset']['total_files_byte_size'] + del self.cr_test_data["research_dataset"]["files"] + del self.cr_test_data["research_dataset"]["total_files_byte_size"] def test_legacy_catalog_pids_are_not_unique(self): # values provided as pid values in legacy catalogs are not required to be unique # within the catalog. - self.cr_test_data['data_catalog'] = LEGACY_CATALOGS[0] - self.cr_test_data['research_dataset']['preferred_identifier'] = 'a' + self.cr_test_data["data_catalog"] = LEGACY_CATALOGS[0] + self.cr_test_data["research_dataset"]["preferred_identifier"] = "a" same_pid_ids = [] for i in range(3): - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], 'a') - same_pid_ids.append(response.data['id']) + self.assertEqual(response.data["research_dataset"]["preferred_identifier"], "a") + same_pid_ids.append(response.data["id"]) # pid can even be same as an existing dataset's pid in an ATT catalog real_pid = CatalogRecord.objects.get(pk=1).preferred_identifier - self.cr_test_data['research_dataset']['preferred_identifier'] = real_pid - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = real_pid + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], real_pid) + self.assertEqual(response.data["research_dataset"]["preferred_identifier"], real_pid) def test_legacy_catalog_pid_must_be_provided(self): # pid cant be empty string - self.cr_test_data['data_catalog'] = LEGACY_CATALOGS[0] - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["data_catalog"] = LEGACY_CATALOGS[0] + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # pid cant be omitted - del self.cr_test_data['research_dataset']['preferred_identifier'] - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + del self.cr_test_data["research_dataset"]["preferred_identifier"] + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_legacy_catalog_pids_update(self): # test setup - self.cr_test_data['data_catalog'] = LEGACY_CATALOGS[0] - self.cr_test_data['research_dataset']['preferred_identifier'] = 'a' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["data_catalog"] = LEGACY_CATALOGS[0] + self.cr_test_data["research_dataset"]["preferred_identifier"] = "a" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # update record. in updates uniqueness also should not be checked modify = response.data real_pid = CatalogRecord.objects.get(pk=1).preferred_identifier - modify['research_dataset']['preferred_identifier'] = real_pid - response = self.client.put('/rest/datasets/%s?include_legacy' % modify['id'], modify, format="json") + modify["research_dataset"]["preferred_identifier"] = real_pid + response = self.client.put( + "/rest/datasets/%s?include_legacy" % modify["id"], modify, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) def test_delete_legacy_catalog_dataset(self): @@ -3424,17 +4113,17 @@ def test_delete_legacy_catalog_dataset(self): """ # test setup - self.cr_test_data['data_catalog'] = LEGACY_CATALOGS[0] - self.cr_test_data['research_dataset']['preferred_identifier'] = 'a' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["data_catalog"] = LEGACY_CATALOGS[0] + self.cr_test_data["research_dataset"]["preferred_identifier"] = "a" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] + cr_id = response.data["id"] # delete record - response = self.client.delete('/rest/datasets/%s?include_legacy' % cr_id, format="json") + response = self.client.delete("/rest/datasets/%s?include_legacy" % cr_id, format="json") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) results_count = CatalogRecord.objects_unfiltered.filter(pk=cr_id).count() - self.assertEqual(results_count, 0, 'record should have been deleted permantly') + self.assertEqual(results_count, 0, "record should have been deleted permantly") class CatalogRecordApiWriteOwnerFields(CatalogRecordApiWriteCommon): @@ -3453,45 +4142,49 @@ def test_metadata_owner_org_is_copied_from_metadata_provider_org(self): """ # create - cr = self.client.get('/rest/datasets/1', format="json").data - cr.pop('id') - cr.pop('identifier') - cr.pop('metadata_owner_org') - cr['research_dataset'].pop('preferred_identifier') - response = self.client.post('/rest/datasets', cr, format="json") + cr = self.client.get("/rest/datasets/1", format="json").data + cr.pop("id") + cr.pop("identifier") + cr.pop("metadata_owner_org") + cr["research_dataset"].pop("preferred_identifier") + response = self.client.post("/rest/datasets", cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['metadata_owner_org'], response.data['metadata_provider_org']) + self.assertEqual( + response.data["metadata_owner_org"], response.data["metadata_provider_org"] + ) # update to null - update is prevented - cr = self.client.get('/rest/datasets/1', format="json").data - original = cr['metadata_owner_org'] - cr['metadata_owner_org'] = None - response = self.client.put('/rest/datasets/1', cr, format="json") + cr = self.client.get("/rest/datasets/1", format="json").data + original = cr["metadata_owner_org"] + cr["metadata_owner_org"] = None + response = self.client.put("/rest/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['metadata_owner_org'], original) + self.assertEqual(response.data["metadata_owner_org"], original) # update with patch, where metadata_owner_org field is absent - value is not reverted back # to metadata_provider_org - response = self.client.patch('/rest/datasets/1', { 'metadata_owner_org': 'abc' }, format="json") + response = self.client.patch( + "/rest/datasets/1", {"metadata_owner_org": "abc"}, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.patch('/rest/datasets/1', { 'contract': 1 }, format="json") - self.assertEqual(response.data['metadata_owner_org'], 'abc') + response = self.client.patch("/rest/datasets/1", {"contract": 1}, format="json") + self.assertEqual(response.data["metadata_owner_org"], "abc") def test_metadata_provider_org_is_readonly_after_creating(self): - cr = self.client.get('/rest/datasets/1', format="json").data - original = cr['metadata_provider_org'] - cr['metadata_provider_org'] = 'changed' - response = self.client.put('/rest/datasets/1', cr, format="json") + cr = self.client.get("/rest/datasets/1", format="json").data + original = cr["metadata_provider_org"] + cr["metadata_provider_org"] = "changed" + response = self.client.put("/rest/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['metadata_provider_org'], original) + self.assertEqual(response.data["metadata_provider_org"], original) def test_metadata_provider_user_is_readonly_after_creating(self): - cr = self.client.get('/rest/datasets/1', format="json").data - original = cr['metadata_provider_user'] - cr['metadata_provider_user'] = 'changed' - response = self.client.put('/rest/datasets/1', cr, format="json") + cr = self.client.get("/rest/datasets/1", format="json").data + original = cr["metadata_provider_user"] + cr["metadata_provider_user"] = "changed" + response = self.client.put("/rest/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['metadata_provider_user'], original) + self.assertEqual(response.data["metadata_provider_user"], original) class CatalogRecordApiEndUserAccess(CatalogRecordApiWriteCommon): @@ -3507,20 +4200,20 @@ def setUp(self): dc = DataCatalog.objects.get(pk=1) catalog_json = dc.catalog_json for identifier in END_USER_ALLOWED_DATA_CATALOGS: - catalog_json['identifier'] = identifier + catalog_json["identifier"] = identifier dc = DataCatalog.objects.create( catalog_json=catalog_json, date_created=get_tz_aware_now_without_micros(), - catalog_record_services_create='testuser,api_auth_user,metax', - catalog_record_services_edit='testuser,api_auth_user,metax', - catalog_record_services_read='testuser,api_auth_user,metax' + catalog_record_services_create="testuser,api_auth_user,metax", + catalog_record_services_edit="testuser,api_auth_user,metax", + catalog_record_services_read="testuser,api_auth_user,metax", ) self.token = get_test_oidc_token() # by default, use the unmodified token. to use a different/modified token # for various test scenarions, alter self.token, and call the below method again - self._use_http_authorization(method='bearer', token=self.token) + self._use_http_authorization(method="bearer", token=self.token) # no reason to test anything related to failed authentication, since failed # authentication stops the request from proceeding anywhere @@ -3528,183 +4221,162 @@ def setUp(self): def _set_cr_owner_to_token_user(self, cr_id): cr = CatalogRecord.objects.get(pk=cr_id) - cr.user_created = self.token['CSCUserName'] - cr.metadata_provider_user = self.token['CSCUserName'] - cr.editor = None # pretend the record was created by user directly + cr.user_created = self.token["CSCUserName"] + cr.metadata_provider_user = self.token["CSCUserName"] cr.force_save() def _set_cr_to_permitted_catalog(self, cr_id): cr = CatalogRecord.objects.get(pk=cr_id) - cr.data_catalog_id = DataCatalog.objects.get(catalog_json__identifier=END_USER_ALLOWED_DATA_CATALOGS[0]).id + cr.data_catalog_id = DataCatalog.objects.get( + catalog_json__identifier=END_USER_ALLOWED_DATA_CATALOGS[0] + ).id cr.force_save() @responses.activate def test_user_can_create_dataset(self): - ''' + """ Ensure end user can create a new dataset, and required fields are automatically placed and the user is only able to affect allowed fields - ''' - user_created = self.token['CSCUserName'] - metadata_provider_user = self.token['CSCUserName'] - metadata_provider_org = self.token['schacHomeOrganization'] - metadata_owner_org = self.token['schacHomeOrganization'] - - self.cr_test_data['data_catalog'] = END_USER_ALLOWED_DATA_CATALOGS[0] # ida - self.cr_test_data['contract'] = 1 - self.cr_test_data['editor'] = { 'nope': 'discarded by metax' } - self.cr_test_data['preservation_description'] = 'discarded by metax' - self.cr_test_data['preservation_reason_description'] = 'discarded by metax' - self.cr_test_data['preservation_state'] = 10 - self.cr_test_data.pop('metadata_provider_user', None) - self.cr_test_data.pop('metadata_provider_org', None) - self.cr_test_data.pop('metadata_owner_org', None) + """ + user_created = self.token["CSCUserName"] + metadata_provider_user = self.token["CSCUserName"] + metadata_provider_org = self.token["schacHomeOrganization"] + metadata_owner_org = self.token["schacHomeOrganization"] + + self.cr_test_data["data_catalog"] = END_USER_ALLOWED_DATA_CATALOGS[0] # ida + self.cr_test_data["contract"] = 1 + self.cr_test_data["preservation_description"] = "discarded by metax" + self.cr_test_data["preservation_reason_description"] = "discarded by metax" + self.cr_test_data["preservation_state"] = 10 + self.cr_test_data.pop("metadata_provider_user", None) + self.cr_test_data.pop("metadata_provider_org", None) + self.cr_test_data.pop("metadata_owner_org", None) # test file permission checking in another test - self.cr_test_data['research_dataset'].pop('files', None) - self.cr_test_data['research_dataset'].pop('directories', None) + self.cr_test_data["research_dataset"].pop("files", None) + self.cr_test_data["research_dataset"].pop("directories", None) - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.content) - self.assertEqual(response.data['user_created'], user_created) - self.assertEqual(response.data['metadata_provider_user'], metadata_provider_user) - self.assertEqual(response.data['metadata_provider_org'], metadata_provider_org) - self.assertEqual(response.data['metadata_owner_org'], metadata_owner_org) - self.assertEqual('contract' in response.data, False) - self.assertEqual('editor' in response.data, False) - self.assertEqual('preservation_description' in response.data, False) - self.assertEqual('preservation_reason_description' in response.data, False) - self.assertEqual(response.data['preservation_state'], 0) + self.assertEqual(response.data["user_created"], user_created) + self.assertEqual(response.data["metadata_provider_user"], metadata_provider_user) + self.assertEqual(response.data["metadata_provider_org"], metadata_provider_org) + self.assertEqual(response.data["metadata_owner_org"], metadata_owner_org) + self.assertEqual("contract" in response.data, False) + self.assertEqual("preservation_description" in response.data, False) + self.assertEqual("preservation_reason_description" in response.data, False) + self.assertEqual(response.data["preservation_state"], 0) @responses.activate def test_user_can_create_datasets_only_to_limited_catalogs(self): - ''' + """ End users should not be able to create datasets for example to harvested data catalogs. - ''' + """ # test file permission checking in another test - self.cr_test_data['research_dataset'].pop('files', None) - self.cr_test_data['research_dataset'].pop('directories', None) + self.cr_test_data["research_dataset"].pop("files", None) + self.cr_test_data["research_dataset"].pop("directories", None) # should not work - self.cr_test_data['data_catalog'] = 1 - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["data_catalog"] = 1 + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) # check error has expected error description - self.assertEqual('selected data catalog' in response.data['detail'][0], True, response.data) + self.assertEqual("selected data catalog" in response.data["detail"][0], True, response.data) # should work # draft catalog cannot be used in V1 api so skip them here - for identifier in [ dc for dc in END_USER_ALLOWED_DATA_CATALOGS if dc != DFT_CATALOG ]: + for identifier in [dc for dc in END_USER_ALLOWED_DATA_CATALOGS if dc != DFT_CATALOG]: if identifier in LEGACY_CATALOGS: - self.cr_test_data['research_dataset']['preferred_identifier'] = 'a' + self.cr_test_data["research_dataset"]["preferred_identifier"] = "a" - self.cr_test_data['data_catalog'] = identifier - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["data_catalog"] = identifier + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) if identifier in LEGACY_CATALOGS: # prevents next test from crashing if legacy catalog is not the last in the list - del self.cr_test_data['research_dataset']['preferred_identifier'] + del self.cr_test_data["research_dataset"]["preferred_identifier"] @responses.activate def test_owner_can_edit_dataset(self): - ''' + """ Ensure end users are able to edit datasets owned by them. Ensure end users can only edit permitted fields. Note: File project permissions should not be checked, since files are not changed. - ''' + """ # create test record - self.cr_test_data['data_catalog'] = END_USER_ALLOWED_DATA_CATALOGS[0] - self.cr_test_data['research_dataset'].pop('files', None) # test file permission checking in another test - self.cr_test_data['research_dataset'].pop('directories', None) - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["data_catalog"] = END_USER_ALLOWED_DATA_CATALOGS[0] + self.cr_test_data["research_dataset"].pop( + "files", None + ) # test file permission checking in another test + self.cr_test_data["research_dataset"].pop("directories", None) + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) modified_data = response.data # research_dataset is the only permitted field to edit - modified_data['research_dataset']['value'] = 112233 - modified_data['contract'] = 1 - modified_data['editor'] = { 'nope': 'discarded by metax' } - modified_data['preservation_description'] = 'discarded by metax' - modified_data['preservation_reason_description'] = 'discarded by metax' - modified_data['preservation_state'] = 10 - - response = self.client.put('/rest/datasets/%d' % modified_data['id'], modified_data, format="json") + modified_data["research_dataset"]["value"] = 112233 + modified_data["contract"] = 1 + modified_data["preservation_description"] = "discarded by metax" + modified_data["preservation_reason_description"] = "discarded by metax" + modified_data["preservation_state"] = 10 + + response = self.client.put( + "/rest/datasets/%d" % modified_data["id"], modified_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['research_dataset']['value'], 112233) # value we set - self.assertEqual(response.data['user_modified'], self.token['CSCUserName']) # set by metax + self.assertEqual(response.data["research_dataset"]["value"], 112233) # value we set + self.assertEqual(response.data["user_modified"], self.token["CSCUserName"]) # set by metax # none of these should have been affected - self.assertEqual('contract' in response.data, False) - self.assertEqual('editor' in response.data, False) - self.assertEqual('preservation_description' in response.data, False) - self.assertEqual('preservation_reason_description' in response.data, False) - self.assertEqual(response.data['preservation_state'], 0) + self.assertEqual("contract" in response.data, False) + self.assertEqual("preservation_description" in response.data, False) + self.assertEqual("preservation_reason_description" in response.data, False) + self.assertEqual(response.data["preservation_state"], 0) @responses.activate def test_owner_can_edit_datasets_only_in_permitted_catalogs(self): - ''' + """ Ensure end users are able to edit datasets only in permitted catalogs, even if they own the record (catalog may be disabled from end user editing for reason or another). - ''' + """ # create test record - self.cr_test_data['data_catalog'] = 1 - self.cr_test_data['user_created'] = self.token['CSCUserName'] - self.cr_test_data['metadata_provider_user'] = self.token['CSCUserName'] - self.cr_test_data.pop('editor', None) + self.cr_test_data["data_catalog"] = 1 + self.cr_test_data["user_created"] = self.token["CSCUserName"] + self.cr_test_data["metadata_provider_user"] = self.token["CSCUserName"] - self._use_http_authorization() # create cr as a service-user - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._use_http_authorization() # create cr as a service-user + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) modified_data = response.data - modified_data['research_dataset']['value'] = 112233 + modified_data["research_dataset"]["value"] = 112233 - self._use_http_authorization(method='bearer', token=self.token) - response = self.client.put('/rest/datasets/%d' % modified_data['id'], modified_data, format="json") + self._use_http_authorization(method="bearer", token=self.token) + response = self.client.put( + "/rest/datasets/%d" % modified_data["id"], modified_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - @responses.activate - def test_owner_can_edit_dataset_check_perms_from_editor_field(self): - ''' - Ensure end user perms are also checked from the field 'editor', which may be - set by .e.g. qvain. - ''' - self.cr_test_data['data_catalog'] = END_USER_ALLOWED_DATA_CATALOGS[0] - self.cr_test_data['user_created'] = 'editor field is checked before this field, so should be ok' - self.cr_test_data['editor'] = { 'owner_id': self.token['CSCUserName'] } - - self._use_http_authorization() # create cr as a service-user to ensure editor-field is set - - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - self._use_http_authorization(method='bearer', token=self.token) - response = self.client.get('/rest/datasets/%d' % response.data['id'], format="json") - modified_data = response.data - modified_data['research_dataset']['value'] = 112233 - - response = self.client.put('/rest/datasets/%d' % response.data['id'], modified_data, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - @responses.activate def test_other_users_cant_edit_dataset(self): - ''' + """ Ensure end users are unable edit datasets not owned by them. - ''' - response = self.client.get('/rest/datasets/1', format="json") + """ + response = self.client.get("/rest/datasets/1", format="json") modified_data = response.data - modified_data['research_dataset']['value'] = 112233 + modified_data["research_dataset"]["value"] = 112233 - response = self.client.put('/rest/datasets/1', modified_data, format="json") + response = self.client.put("/rest/datasets/1", modified_data, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.put('/rest/datasets', [modified_data], format="json") + response = self.client.put("/rest/datasets", [modified_data], format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # ^ individual errors do not have error codes, only the general request # has an error code for a failed request. @@ -3713,90 +4385,100 @@ def test_other_users_cant_edit_dataset(self): def test_user_can_delete_dataset(self): self._set_cr_owner_to_token_user(1) self._set_cr_to_permitted_catalog(1) - response = self.client.delete('/rest/datasets/1', format="json") + response = self.client.delete("/rest/datasets/1", format="json") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) @responses.activate def test_user_file_permissions_are_checked_during_dataset_create(self): - ''' + """ Ensure user's association with a project is checked during dataset create when attaching files or directories to a dataset. - ''' + """ # try creating without proper permisisons - self.cr_test_data['data_catalog'] = END_USER_ALLOWED_DATA_CATALOGS[0] # ida - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["data_catalog"] = END_USER_ALLOWED_DATA_CATALOGS[0] # ida + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.content) # add project membership to user's token and try again - file_identifier = self.cr_test_data['research_dataset']['files'][0]['identifier'] + file_identifier = self.cr_test_data["research_dataset"]["files"][0]["identifier"] project_identifier = File.objects.get(identifier=file_identifier).project_identifier - self.token['group_names'].append('IDA01:%s' % project_identifier) - self._use_http_authorization(method='bearer', token=self.token) + self.token["group_names"].append("IDA01:%s" % project_identifier) + self._use_http_authorization(method="bearer", token=self.token) - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.content) @responses.activate def test_user_file_permissions_are_checked_during_dataset_update(self): - ''' + """ Ensure user's association with a project is checked during dataset update when attaching files or directories to a dataset. The permissions should be checked only for changed files (newly added, or removed). - ''' + """ # get some files to add to another dataset - response = self.client.get('/rest/datasets/2', format="json") - new_files = response.data['research_dataset']['files'] + response = self.client.get("/rest/datasets/2", format="json") + new_files = response.data["research_dataset"]["files"] # this is the dataset we'll modify self._set_cr_owner_to_token_user(1) self._set_cr_to_permitted_catalog(1) - response = self.client.get('/rest/datasets/1', format="json") + response = self.client.get("/rest/datasets/1", format="json") # ensure the files really are new for f in new_files: - for existing_f in response.data['research_dataset']['files']: - assert f['identifier'] != existing_f['identifier'], 'test preparation failure, files should differ' + for existing_f in response.data["research_dataset"]["files"]: + assert ( + f["identifier"] != existing_f["identifier"] + ), "test preparation failure, files should differ" modified_data = response.data - modified_data['research_dataset']['files'].extend(new_files) + modified_data["research_dataset"]["files"].extend(new_files) # should fail, since user's token has no permission for the newly added files - response = self.client.put('/rest/datasets/1', modified_data, format="json") + response = self.client.put("/rest/datasets/1", modified_data, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.content) # add project membership to user's token and try again - project_identifier = File.objects.get(identifier=new_files[0]['identifier']).project_identifier - self.token['group_names'].append('IDA01:%s' % project_identifier) - self._use_http_authorization(method='bearer', token=self.token) + project_identifier = File.objects.get( + identifier=new_files[0]["identifier"] + ).project_identifier + self.token["group_names"].append("IDA01:%s" % project_identifier) + self._use_http_authorization(method="bearer", token=self.token) - response = self.client.put('/rest/datasets/1', modified_data, format="json") + response = self.client.put("/rest/datasets/1", modified_data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.content) @responses.activate def test_owner_receives_unfiltered_dataset_data(self): - ''' + """ The general public will have some fields filtered out from the dataset, in order to protect sensitive data. The owner of a dataset however should always receive full data. - ''' + """ self._set_cr_owner_to_token_user(1) def _check_fields(obj): - for sensitive_field in ['email', 'telephone', 'phone']: - self.assertEqual(sensitive_field in obj['research_dataset']['curator'][0], True, - 'field %s should be present' % sensitive_field) + for sensitive_field in ["email", "telephone", "phone"]: + self.assertEqual( + sensitive_field in obj["research_dataset"]["curator"][0], + True, + "field %s should be present" % sensitive_field, + ) for cr in CatalogRecord.objects.filter(pk=1): - cr.research_dataset['curator'][0].update({ - 'email': 'email@mail.com', - 'phone': '123124', - 'telephone': '123124', - }) + cr.research_dataset["curator"][0].update( + { + "email": "email@mail.com", + "phone": "123124", + "telephone": "123124", + } + ) cr.force_save() - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) _check_fields(response.data) + class CatalogRecordExternalServicesAccess(CatalogRecordApiWriteCommon): """ @@ -3809,187 +4491,217 @@ def setUp(self): """ super().setUp() - self.dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema='att').first() - self.dc.catalog_json['identifier'] = EXT_CATALOG - self.dc.catalog_json['harvested'] = True - self.dc.catalog_record_services_create = 'external' - self.dc.catalog_record_services_edit = 'external' + self.dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema="att").first() + self.dc.catalog_json["identifier"] = EXT_CATALOG + self.dc.catalog_json["harvested"] = True + self.dc.catalog_record_services_create = "external" + self.dc.catalog_record_services_edit = "external" self.dc.force_save() - self.cr_test_data['data_catalog'] = self.dc.catalog_json['identifier'] - del self.cr_test_data['research_dataset']['files'] - del self.cr_test_data['research_dataset']['total_files_byte_size'] + self.cr_test_data["data_catalog"] = self.dc.catalog_json["identifier"] + del self.cr_test_data["research_dataset"]["files"] + del self.cr_test_data["research_dataset"]["total_files_byte_size"] - self._use_http_authorization(username=django_settings.API_EXT_USER['username'], - password=django_settings.API_EXT_USER['password']) + self._use_http_authorization( + username=django_settings.API_EXT_USER["username"], + password=django_settings.API_EXT_USER["password"], + ) def test_external_service_can_not_read_all_metadata_in_other_catalog(self): - ''' External service should get the same output from someone elses catalog than anonymous user ''' + """ External service should get the same output from someone elses catalog than anonymous user """ # create a catalog that does not belong to our external service dc2 = DataCatalog.objects.get(pk=2) - dc2.catalog_json['identifier'] = 'Some other catalog' - dc2.catalog_record_services_read = 'metax' + dc2.catalog_json["identifier"] = "Some other catalog" + dc2.catalog_record_services_read = "metax" dc2.force_save() # Create a catalog record that belongs to some other user & our catalog nr2 cr = CatalogRecord.objects.get(pk=12) - cr.user_created = '#### Some owner who is not you ####' - cr.metadata_provider_user = '#### Some owner who is not you ####' + cr.user_created = "#### Some owner who is not you ####" + cr.metadata_provider_user = "#### Some owner who is not you ####" cr.data_catalog = dc2 - cr.editor = None - cr.research_dataset['access_rights']['access_type']['identifier'] = ACCESS_TYPES['restricted'] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = ACCESS_TYPES[ + "restricted" + ] cr.force_save() # Let's try to return the data with our external services credentials - response_service_user = self.client.get('/rest/datasets/12') - self.assertEqual(response_service_user.status_code, status.HTTP_200_OK, response_service_user.data) + response_service_user = self.client.get("/rest/datasets/12") + self.assertEqual( + response_service_user.status_code, + status.HTTP_200_OK, + response_service_user.data, + ) # Test access as unauthenticated user self.client._credentials = {} - response_anonymous = self.client.get('/rest/datasets/12') - self.assertEqual(response_anonymous.status_code, status.HTTP_200_OK, response_anonymous.data) + response_anonymous = self.client.get("/rest/datasets/12") + self.assertEqual( + response_anonymous.status_code, status.HTTP_200_OK, response_anonymous.data + ) - self.assertEqual(response_anonymous.data, response_service_user.data, - "External service with no read-rights should not see any more metadata than anonymous user from a catalog") + self.assertEqual( + response_anonymous.data, + response_service_user.data, + "External service with no read-rights should not see any more metadata than anonymous user from a catalog", + ) def assert_catalog_record_not_open_access(self, cr): from metax_api.models.catalog_record import ACCESS_TYPES - access_type = cr['research_dataset'].get('access_rights', {}).get('access_type', {}).get('identifier', '') - assert(access_type != ACCESS_TYPES['open']) + + access_type = ( + cr["research_dataset"] + .get("access_rights", {}) + .get("access_type", {}) + .get("identifier", "") + ) + assert access_type != ACCESS_TYPES["open"] def test_external_service_can_add_catalog_record_to_own_catalog(self): - self.cr_test_data['research_dataset']['preferred_identifier'] = '123456' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = "123456" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], '123456') + self.assertEqual(response.data["research_dataset"]["preferred_identifier"], "123456") def test_external_service_can_update_catalog_record_in_own_catalog(self): - self.cr_test_data['research_dataset']['preferred_identifier'] = '123456' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = "123456" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], '123456') + self.assertEqual(response.data["research_dataset"]["preferred_identifier"], "123456") - cr_id = response.data['id'] - self.cr_test_data['research_dataset']['preferred_identifier'] = '654321' - response = self.client.put('/rest/datasets/{}'.format(cr_id), self.cr_test_data, format="json") + cr_id = response.data["id"] + self.cr_test_data["research_dataset"]["preferred_identifier"] = "654321" + response = self.client.put( + "/rest/datasets/{}".format(cr_id), self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], '654321') + self.assertEqual(response.data["research_dataset"]["preferred_identifier"], "654321") def test_external_service_can_delete_catalog_record_from_own_catalog(self): - self.cr_test_data['research_dataset']['preferred_identifier'] = '123456' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = "123456" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") - cr_id = response.data['id'] - response = self.client.delete('/rest/datasets/{}'.format(cr_id)) + cr_id = response.data["id"] + response = self.client.delete("/rest/datasets/{}".format(cr_id)) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - response = self.client.get('/rest/datasets/{}'.format(cr_id), format="json") - self.assertEqual('not found' in response.json()['detail'].lower(), True) + response = self.client.get("/rest/datasets/{}".format(cr_id), format="json") + self.assertEqual("not found" in response.json()["detail"].lower(), True) def test_external_service_can_not_add_catalog_record_to_other_catalog(self): - dc = self._get_object_from_test_data('datacatalog', requested_index=1) - self.cr_test_data['data_catalog'] = dc['catalog_json']['identifier'] - self.cr_test_data['research_dataset']['preferred_identifier'] = 'temp-pid' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + dc = self._get_object_from_test_data("datacatalog", requested_index=1) + self.cr_test_data["data_catalog"] = dc["catalog_json"]["identifier"] + self.cr_test_data["research_dataset"]["preferred_identifier"] = "temp-pid" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) def test_external_service_can_not_update_catalog_record_in_other_catalog(self): - response = self.client.put('/rest/datasets/1', {}, format="json") + response = self.client.put("/rest/datasets/1", {}, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) def test_external_service_can_not_delete_catalog_record_from_other_catalog(self): - response = self.client.delete('/rest/datasets/1') + response = self.client.delete("/rest/datasets/1") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) def test_harvested_catalogs_must_have_preferred_identifier_create(self): # create without preferred identifier - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('must have preferred identifier' in - response.data['research_dataset']['preferred_identifier'][0], True) + self.assertEqual( + "must have preferred identifier" + in response.data["research_dataset"]["preferred_identifier"][0], + True, + ) - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('must have preferred identifier' in - response.data['research_dataset']['preferred_identifier'][0], True) + self.assertEqual( + "must have preferred identifier" + in response.data["research_dataset"]["preferred_identifier"][0], + True, + ) -@unittest.skipIf(django_settings.REMS['ENABLED'] is not True, 'Only run if REMS is enabled') +@unittest.skipIf(django_settings.REMS["ENABLED"] is not True, "Only run if REMS is enabled") class CatalogRecordApiWriteREMS(CatalogRecordApiWriteCommon): cache = RedisClient() rf = RDM.get_reference_data(cache) # get by code to prevent failures if list ordering changes - access_permit = [type for type in rf['reference_data']['access_type'] if type['code'] == 'permit'][0] - access_open = [type for type in rf['reference_data']['access_type'] if type['code'] == 'open'][0] + access_permit = [ + type for type in rf["reference_data"]["access_type"] if type["code"] == "permit" + ][0] + access_open = [type for type in rf["reference_data"]["access_type"] if type["code"] == "open"][ + 0 + ] permit_rights = { # license type does not matter "license": [ { - "title": rf['reference_data']['license'][0]['label'], - "identifier": rf['reference_data']['license'][0]['uri'] + "title": rf["reference_data"]["license"][0]["label"], + "identifier": rf["reference_data"]["license"][0]["uri"], } ], "access_type": { - "in_scheme": access_permit['scheme'], - "identifier": access_permit['uri'], - "pref_label": access_permit['label'] - } + "in_scheme": access_permit["scheme"], + "identifier": access_permit["uri"], + "pref_label": access_permit["label"], + }, } open_rights = { "access_type": { - "in_scheme": access_open['scheme'], - "identifier": access_open['uri'], - "pref_label": access_open['label'] + "in_scheme": access_open["scheme"], + "identifier": access_open["uri"], + "pref_label": access_open["label"], } } # any other than what is included in permit_rights is sufficient - other_license = rf['reference_data']['license'][1] + other_license = rf["reference_data"]["license"][1] def setUp(self): super().setUp() # Create ida data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) + dc = self._get_object_from_test_data("datacatalog", requested_index=0) dc_id = IDA_CATALOG - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/datacatalogs', dc, format="json") + dc["catalog_json"]["identifier"] = dc_id + self.client.post("/rest/datacatalogs", dc, format="json") # token for end user access self.token = get_test_oidc_token(new_proxy=True) # mock successful rems access for creation, add fails later if needed. # Not using regex to allow individual access failures - for entity in ['user', 'workflow', 'license', 'resource', 'catalogue-item']: - self._mock_rems_write_access_succeeds('POST', entity, 'create') + for entity in ["user", "workflow", "license", "resource", "catalogue-item"]: + self._mock_rems_write_access_succeeds("POST", entity, "create") - self._mock_rems_read_access_succeeds('license') + self._mock_rems_read_access_succeeds("license") # mock successful rems access for deletion. Add fails later - for entity in ['catalogue-item', 'workflow', 'resource']: - self._mock_rems_write_access_succeeds(method='PUT', entity=entity, action='archived') - self._mock_rems_write_access_succeeds(method='PUT', entity=entity, action='enabled') + for entity in ["catalogue-item", "workflow", "resource"]: + self._mock_rems_write_access_succeeds(method="PUT", entity=entity, action="archived") + self._mock_rems_write_access_succeeds(method="PUT", entity=entity, action="enabled") - self._mock_rems_read_access_succeeds('catalogue-item') - self._mock_rems_read_access_succeeds('application') - self._mock_rems_write_access_succeeds(method='POST', entity='application', action='close') + self._mock_rems_read_access_succeeds("catalogue-item") + self._mock_rems_read_access_succeeds("application") + self._mock_rems_write_access_succeeds(method="POST", entity="application", action="close") responses.add( responses.GET, f"{django_settings.REMS['BASE_URL']}/health", - json={'healthy': True}, - status=200 + json={"healthy": True}, + status=200, ) def _get_access_granter(self, malformed=False): @@ -3999,7 +4711,7 @@ def _get_access_granter(self, malformed=False): access_granter = { "userid": "testcaseuser" if not malformed else 1234, "name": "Test User", - "email": "testcase@user.com" + "email": "testcase@user.com", } return access_granter @@ -4010,23 +4722,23 @@ def _mock_rems_write_access_succeeds(self, method, entity, action): entity: REMS entity [application, catalogue-item, license, resource, user, workflow] action: Action taken to entity [archived, close, create, edit, enabled] """ - req_type = responses.POST if method == 'POST' else responses.PUT + req_type = responses.POST if method == "POST" else responses.PUT body = {"success": True} - if method == 'POST' and action != 'close': + if method == "POST" and action != "close": # action condition needed because applications are closed with POST method - body['id'] = 6 + body["id"] = 6 responses.add( req_type, f"{django_settings.REMS['BASE_URL']}/{entity}s/{action}", json=body, - status=200 + status=200, ) def _mock_rems_read_access_succeeds(self, entity): - if entity == 'license': + if entity == "license": resp = [ { "id": 7, @@ -4035,14 +4747,14 @@ def _mock_rems_read_access_succeeds(self, entity): "archived": False, "localizations": { "fi": { - "title": self.rf['reference_data']['license'][0]['label']['fi'], - "textcontent": self.rf['reference_data']['license'][0]['uri'] + "title": self.rf["reference_data"]["license"][0]["label"]["fi"], + "textcontent": self.rf["reference_data"]["license"][0]["uri"], }, "und": { - "title": self.rf['reference_data']['license'][0]['label']['und'], - "textcontent": self.rf['reference_data']['license'][0]['uri'] - } - } + "title": self.rf["reference_data"]["license"][0]["label"]["und"], + "textcontent": self.rf["reference_data"]["license"][0]["uri"], + }, + }, }, { "id": 8, @@ -4051,14 +4763,14 @@ def _mock_rems_read_access_succeeds(self, entity): "archived": False, "localizations": { "en": { - "title": self.rf['reference_data']['license'][1]['label']['en'], - "textcontent": self.rf['reference_data']['license'][1]['uri'] + "title": self.rf["reference_data"]["license"][1]["label"]["en"], + "textcontent": self.rf["reference_data"]["license"][1]["uri"], } - } - } + }, + }, ] - elif entity == 'catalogue-item': + elif entity == "catalogue-item": resp = [ { "archived": False, @@ -4067,7 +4779,7 @@ def _mock_rems_read_access_succeeds(self, entity): "id": 18, "langcode": "en", "title": "Removal test", - "infourl": "https://url.to.etsin.fi" + "infourl": "https://url.to.etsin.fi", } }, "resource-id": 19, @@ -4078,113 +4790,97 @@ def _mock_rems_read_access_succeeds(self, entity): "id": 18, "expired": False, "end": None, - "enabled": True + "enabled": True, } ] - elif entity == 'application': + elif entity == "application": # only mock relevant data resp = [ { - 'application/workflow': { - 'workflow.dynamic/handlers': [ - { - 'userid': 'somehandler' - } - ] + "application/workflow": { + "workflow.dynamic/handlers": [{"userid": "somehandler"}] }, "application/id": 3, - 'application/applicant': { - 'userid': 'someapplicant' - }, + "application/applicant": {"userid": "someapplicant"}, "application/resources": [ { - "catalogue-item/title": { - "en": "Removal test" - }, + "catalogue-item/title": {"en": "Removal test"}, "resource/ext-id": "some:pref:id", - "catalogue-item/id": 5 + "catalogue-item/id": 5, } ], - "application/state": 'application.state/draft' + "application/state": "application.state/draft", }, { - 'application/workflow': { - 'workflow.dynamic/handlers': [ - { - 'userid': 'someid' - } - ] - }, + "application/workflow": {"workflow.dynamic/handlers": [{"userid": "someid"}]}, "application/id": 2, - 'application/applicant': { - 'userid': 'someotherapplicant' - }, + "application/applicant": {"userid": "someotherapplicant"}, "application/resources": [ { - "catalogue-item/title": { - "en": "Removal test" - }, + "catalogue-item/title": {"en": "Removal test"}, "resource/ext-id": "some:pref:id", - "catalogue-item/id": 5 + "catalogue-item/id": 5, } ], - "application/state": 'application.state/approved' + "application/state": "application.state/approved", }, { - 'application/workflow': { - 'workflow.dynamic/handlers': [ - { - 'userid': 'remsuid' - } - ] - }, + "application/workflow": {"workflow.dynamic/handlers": [{"userid": "remsuid"}]}, "application/id": 1, - 'application/applicant': { - 'userid': 'someapplicant' - }, + "application/applicant": {"userid": "someapplicant"}, "application/resources": [ { - "catalogue-item/title": { - "en": "Removal test" - }, - "resource/ext-id": 'Same:title:with:different:catalogue:item', - "catalogue-item/id": 18 + "catalogue-item/title": {"en": "Removal test"}, + "resource/ext-id": "Same:title:with:different:catalogue:item", + "catalogue-item/id": 18, } ], - "application/state": 'application.state/draft' - } + "application/state": "application.state/draft", + }, ] responses.add( responses.GET, f"{django_settings.REMS['BASE_URL']}/{entity}s", json=resp, - status=200 + status=200, ) - def _mock_rems_access_return_403(self, method, entity, action=''): + def _mock_rems_access_return_403(self, method, entity, action=""): """ Works also for GET method since failure responses from rems are identical for write and read operations """ - req_type = responses.POST if method == 'POST' else responses.PUT if method == 'PUT' else responses.GET + req_type = ( + responses.POST + if method == "POST" + else responses.PUT + if method == "PUT" + else responses.GET + ) responses.replace( req_type, f"{django_settings.REMS['BASE_URL']}/{entity}s/{action}", - status=403 # anything else than 200 is a fail + status=403, # anything else than 200 is a fail ) - def _mock_rems_access_return_error(self, method, entity, action=''): + def _mock_rems_access_return_error(self, method, entity, action=""): """ operation status is defined in the body so 200 response can also be failure. """ - req_type = responses.POST if method == 'POST' else responses.PUT if method == 'PUT' else responses.GET + req_type = ( + responses.POST + if method == "POST" + else responses.PUT + if method == "PUT" + else responses.GET + ) errors = [ { "type": "some kind of identifier of this error", - "somedetail": "entity identifier the error is conserning" + "somedetail": "entity identifier the error is conserning", } ] @@ -4192,30 +4888,36 @@ def _mock_rems_access_return_error(self, method, entity, action=''): req_type, f"{django_settings.REMS['BASE_URL']}/{entity}s/{action}", json={"success": False, "errors": errors}, - status=200 + status=200, ) - def _mock_rems_access_crashes(self, method, entity, action=''): + def _mock_rems_access_crashes(self, method, entity, action=""): """ Crash happens for example if there is a network error. Can be used for GET also """ - req_type = responses.POST if method == 'POST' else responses.PUT if method == 'PUT' else responses.GET + req_type = ( + responses.POST + if method == "POST" + else responses.PUT + if method == "PUT" + else responses.GET + ) responses.replace( req_type, f"{django_settings.REMS['BASE_URL']}/{entity}s/{action}", - body=Exception('REMS_service should catch this one also') + body=Exception("REMS_service should catch this one also"), ) def _create_new_rems_dataset(self): """ Modifies catalog record to be REMS managed and post it to Metax """ - self.cr_test_data['research_dataset']['access_rights'] = self.permit_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG - self.cr_test_data['access_granter'] = self._get_access_granter() + self.cr_test_data["research_dataset"]["access_rights"] = self.permit_rights + self.cr_test_data["data_catalog"] = IDA_CATALOG + self.cr_test_data["access_granter"] = self._get_access_granter() - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") return response @@ -4226,26 +4928,32 @@ def test_creating_permit_dataset_creates_catalogue_item_service_succeeds(self): """ response = self._create_new_rems_dataset() self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue(response.data.get('rems_identifier') is not None, 'rems_identifier should be present') - self.assertTrue(response.data.get('access_granter') is not None, 'access_granter should be present') + self.assertTrue( + response.data.get("rems_identifier") is not None, + "rems_identifier should be present", + ) + self.assertTrue( + response.data.get("access_granter") is not None, + "access_granter should be present", + ) @responses.activate def test_creating_permit_dataset_creates_catalogue_item_service_fails_1(self): """ Test unsuccessful rems access """ - self._mock_rems_access_return_403('POST', 'workflow', 'create') + self._mock_rems_access_return_403("POST", "workflow", "create") response = self._create_new_rems_dataset() self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) - self.assertTrue('failed to publish updates' in response.data['detail'][0], response.data) + self.assertTrue("failed to publish updates" in response.data["detail"][0], response.data) @responses.activate def test_creating_permit_dataset_creates_catalogue_item_service_fails_2(self): """ Test unsuccessful rems access """ - self._mock_rems_access_return_error('POST', 'catalogue-item', 'create') + self._mock_rems_access_return_error("POST", "catalogue-item", "create") response = self._create_new_rems_dataset() self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) @@ -4255,11 +4963,11 @@ def test_creating_permit_dataset_creates_catalogue_item_service_fails_3(self): """ Test unsuccessful rems access """ - self._mock_rems_access_crashes('POST', 'resource', 'create') + self._mock_rems_access_crashes("POST", "resource", "create") response = self._create_new_rems_dataset() self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) - self.assertTrue('failed to publish updates' in response.data['detail'][0], response.data) + self.assertTrue("failed to publish updates" in response.data["detail"][0], response.data) @responses.activate def test_changing_dataset_to_permit_creates_new_catalogue_item_succeeds(self): @@ -4268,40 +4976,46 @@ def test_changing_dataset_to_permit_creates_new_catalogue_item_succeeds(self): """ # create dataset without rems managed access - self.cr_test_data['research_dataset']['access_rights'] = self.open_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG + self.cr_test_data["research_dataset"]["access_rights"] = self.open_rights + self.cr_test_data["data_catalog"] = IDA_CATALOG - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # change to rems managed cr = response.data - cr['research_dataset']['access_rights'] = self.permit_rights - cr['access_granter'] = self._get_access_granter() + cr["research_dataset"]["access_rights"] = self.permit_rights + cr["access_granter"] = self._get_access_granter() response = self.client.put(f'/rest/datasets/{cr["id"]}', cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data.get('rems_identifier') is not None, 'rems_identifier should be present') - self.assertTrue(response.data.get('access_granter') is not None, 'access_granter should be present') + self.assertTrue( + response.data.get("rems_identifier") is not None, + "rems_identifier should be present", + ) + self.assertTrue( + response.data.get("access_granter") is not None, + "access_granter should be present", + ) @responses.activate def test_changing_dataset_to_permit_creates_new_catalogue_item_fails(self): """ Test error handling on metax update operation """ - self._mock_rems_access_return_error('POST', 'user', 'create') + self._mock_rems_access_return_error("POST", "user", "create") # create dataset without rems managed access - self.cr_test_data['research_dataset']['access_rights'] = self.open_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG + self.cr_test_data["research_dataset"]["access_rights"] = self.open_rights + self.cr_test_data["data_catalog"] = IDA_CATALOG - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # change to rems managed cr = response.data - cr['research_dataset']['access_rights'] = self.permit_rights - cr['access_granter'] = self._get_access_granter() + cr["research_dataset"]["access_rights"] = self.permit_rights + cr["access_granter"] = self._get_access_granter() response = self.client.put(f'/rest/datasets/{cr["id"]}', cr, format="json") self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) @@ -4312,7 +5026,7 @@ def test_changing_access_type_to_other_closes_rems_entities_succeeds(self): self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr = response.data - cr['research_dataset']['access_rights'] = self.open_rights + cr["research_dataset"]["access_rights"] = self.open_rights response = self.client.put(f'/rest/datasets/{cr["id"]}', cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) @@ -4322,10 +5036,10 @@ def test_changing_access_type_to_other_closes_rems_entities_fails(self): response = self._create_new_rems_dataset() self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self._mock_rems_access_return_error('POST', 'application', 'close') + self._mock_rems_access_return_error("POST", "application", "close") cr = response.data - cr['research_dataset']['access_rights'] = self.open_rights + cr["research_dataset"]["access_rights"] = self.open_rights response = self.client.put(f'/rest/datasets/{cr["id"]}', cr, format="json") self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) @@ -4341,11 +5055,11 @@ def test_changing_dataset_license_updates_rems(self): cr_before = response.data - rems_id_before = cr_before['rems_identifier'] - cr_before['research_dataset']['access_rights']['license'] = [ + rems_id_before = cr_before["rems_identifier"] + cr_before["research_dataset"]["access_rights"]["license"] = [ { - "title": self.other_license['label'], - "identifier": self.other_license['uri'] + "title": self.other_license["label"], + "identifier": self.other_license["uri"], } ] @@ -4353,7 +5067,11 @@ def test_changing_dataset_license_updates_rems(self): self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr_after = response.data - self.assertNotEqual(rems_id_before, cr_after['rems_identifier'], 'REMS identifier should have been changed') + self.assertNotEqual( + rems_id_before, + cr_after["rems_identifier"], + "REMS identifier should have been changed", + ) @responses.activate def test_changing_license_dont_allow_access_granter_changes(self): @@ -4366,18 +5084,20 @@ def test_changing_license_dont_allow_access_granter_changes(self): cr_before = response.data - cr_before['access_granter']['userid'] = 'newid' - cr_before['research_dataset']['access_rights']['license'] = [ - { - "identifier": self.other_license['uri'] - } + cr_before["access_granter"]["userid"] = "newid" + cr_before["research_dataset"]["access_rights"]["license"] = [ + {"identifier": self.other_license["uri"]} ] response = self.client.put(f'/rest/datasets/{cr_before["id"]}', cr_before, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr_after = response.data - self.assertNotEqual('newid', cr_after['access_granter']['userid'], 'userid should not have been changed') + self.assertNotEqual( + "newid", + cr_after["access_granter"]["userid"], + "userid should not have been changed", + ) @responses.activate def test_deleting_license_updates_rems(self): @@ -4389,14 +5109,20 @@ def test_deleting_license_updates_rems(self): cr_before = response.data - cr_before['research_dataset']['access_rights'].pop('license') + cr_before["research_dataset"]["access_rights"].pop("license") response = self.client.put(f'/rest/datasets/{cr_before["id"]}', cr_before, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr_after = response.data - self.assertTrue(cr_after.get('rems_identifier') is None, 'REMS identifier should have been deleted') - self.assertTrue(cr_after.get('access_granter') is None, 'access_granter should have been deleted') + self.assertTrue( + cr_after.get("rems_identifier") is None, + "REMS identifier should have been deleted", + ) + self.assertTrue( + cr_after.get("access_granter") is None, + "access_granter should have been deleted", + ) @responses.activate def test_creating_permit_dataset_creates_catalogue_item_end_user(self): @@ -4404,20 +5130,20 @@ def test_creating_permit_dataset_creates_catalogue_item_end_user(self): Tests that catalogue item in REMS is created correctly on permit dataset creation. User information is fetch'd from token. """ - self._set_http_authorization('owner') + self._set_http_authorization("owner") # modify catalog record - self.cr_test_data['user_created'] = self.token['CSCUserName'] - self.cr_test_data['metadata_provider_user'] = self.token['CSCUserName'] - self.cr_test_data['metadata_provider_org'] = self.token['schacHomeOrganization'] - self.cr_test_data['metadata_owner_org'] = self.token['schacHomeOrganization'] - self.cr_test_data['research_dataset']['access_rights'] = self.permit_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG + self.cr_test_data["user_created"] = self.token["CSCUserName"] + self.cr_test_data["metadata_provider_user"] = self.token["CSCUserName"] + self.cr_test_data["metadata_provider_org"] = self.token["schacHomeOrganization"] + self.cr_test_data["metadata_owner_org"] = self.token["schacHomeOrganization"] + self.cr_test_data["research_dataset"]["access_rights"] = self.permit_rights + self.cr_test_data["data_catalog"] = IDA_CATALOG # end user doesn't have permissions to the files and they are also not needed in this test - del self.cr_test_data['research_dataset']['files'] + del self.cr_test_data["research_dataset"]["files"] - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) @responses.activate @@ -4425,15 +5151,15 @@ def test_deleting_permit_dataset_removes_catalogue_item_succeeds(self): response = self._create_new_rems_dataset() self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] + cr_id = response.data["id"] # delete dataset - response = self.client.delete(f'/rest/datasets/{cr_id}') + response = self.client.delete(f"/rest/datasets/{cr_id}") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - cr = self.client.get(f'/rest/datasets/{cr_id}?removed').data - self.assertTrue(cr.get('rems_identifier') is None, 'rems_identifier should not be present') - self.assertTrue(cr.get('access_granter') is None, 'access_granter should not be present') + cr = self.client.get(f"/rest/datasets/{cr_id}?removed").data + self.assertTrue(cr.get("rems_identifier") is None, "rems_identifier should not be present") + self.assertTrue(cr.get("access_granter") is None, "access_granter should not be present") @responses.activate def test_deleting_permit_dataset_removes_catalogue_item_fails(self): @@ -4441,7 +5167,7 @@ def test_deleting_permit_dataset_removes_catalogue_item_fails(self): self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # delete dataset - self._mock_rems_access_return_error('PUT', 'catalogue-item', 'enabled') + self._mock_rems_access_return_error("PUT", "catalogue-item", "enabled") response = self.client.delete(f'/rest/datasets/{response.data["id"]}') self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) @@ -4453,12 +5179,20 @@ def test_deprecating_permit_dataset_removes_catalogue_item_succeeds(self): cr_before = response.data # deprecate dataset - response = self.client.delete(f"/rest/files/{cr_before['research_dataset']['files'][0]['identifier']}") + response = self.client.delete( + f"/rest/files/{cr_before['research_dataset']['files'][0]['identifier']}" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr_after = self.client.get(f'/rest/datasets/{cr_before["id"]}').data - self.assertTrue(cr_after.get('rems_identifier') is None, 'rems_identifier should not be present') - self.assertTrue(cr_after.get('access_granter') is None, 'access_granter should not be present') + self.assertTrue( + cr_after.get("rems_identifier") is None, + "rems_identifier should not be present", + ) + self.assertTrue( + cr_after.get("access_granter") is None, + "access_granter should not be present", + ) @responses.activate def test_deprecating_permit_dataset_removes_catalogue_item_fails(self): @@ -4466,11 +5200,13 @@ def test_deprecating_permit_dataset_removes_catalogue_item_fails(self): self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # deprecate dataset - self._mock_rems_access_crashes('PUT', 'workflow', 'archived') + self._mock_rems_access_crashes("PUT", "workflow", "archived") - response = self.client.delete(f"/rest/files/{response.data['research_dataset']['files'][0]['identifier']}") + response = self.client.delete( + f"/rest/files/{response.data['research_dataset']['files'][0]['identifier']}" + ) self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) - self.assertTrue('failed to publish' in response.data['detail'][0], response.data) + self.assertTrue("failed to publish" in response.data["detail"][0], response.data) def test_missing_access_granter(self): """ @@ -4479,70 +5215,78 @@ def test_missing_access_granter(self): """ # test on create - self.cr_test_data['research_dataset']['access_rights'] = self.permit_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG + self.cr_test_data["research_dataset"]["access_rights"] = self.permit_rights + self.cr_test_data["data_catalog"] = IDA_CATALOG - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('access_granter' in response.data['detail'][0], response.data) + self.assertTrue("access_granter" in response.data["detail"][0], response.data) # test on update - self.cr_test_data['research_dataset']['access_rights'] = self.open_rights - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["access_rights"] = self.open_rights + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr = response.data - cr['research_dataset']['access_rights'] = self.permit_rights + cr["research_dataset"]["access_rights"] = self.permit_rights response = self.client.put(f'/rest/datasets/{cr["id"]}', cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('access_granter' in response.data['detail'][0], response.data) + self.assertTrue("access_granter" in response.data["detail"][0], response.data) def test_bad_access_granter_parameter(self): """ Access_granter values must be strings """ - self.cr_test_data['research_dataset']['access_rights'] = self.permit_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG - self.cr_test_data['access_granter'] = self._get_access_granter(malformed=True) + self.cr_test_data["research_dataset"]["access_rights"] = self.permit_rights + self.cr_test_data["data_catalog"] = IDA_CATALOG + self.cr_test_data["access_granter"] = self._get_access_granter(malformed=True) - response = self.client.post( - '/rest/datasets', - self.cr_test_data, - format="json" - ) + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('must be string' in response.data['detail'][0], response.data) + self.assertTrue("must be string" in response.data["detail"][0], response.data) def test_missing_license_in_dataset(self): """ License is required when dataset is REMS managed """ - self.cr_test_data['research_dataset']['access_rights'] = deepcopy(self.permit_rights) - del self.cr_test_data['research_dataset']['access_rights']['license'] - self.cr_test_data['data_catalog'] = IDA_CATALOG + self.cr_test_data["research_dataset"]["access_rights"] = deepcopy(self.permit_rights) + del self.cr_test_data["research_dataset"]["access_rights"]["license"] + self.cr_test_data["data_catalog"] = IDA_CATALOG response = self.client.post( - f'/rest/datasets?access_granter={self._get_access_granter()}', + f"/rest/datasets?access_granter={self._get_access_granter()}", self.cr_test_data, - format="json" + format="json", ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('must define license' in response.data['detail'][0], response.data) + self.assertTrue("must define license" in response.data["detail"][0], response.data) @responses.activate def test_only_return_rems_info_to_privileged(self): - self._set_http_authorization('service') + self._set_http_authorization("service") response = self._create_new_rems_dataset() self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue(response.data.get('rems_identifier') is not None, 'rems_identifier should be returned to owner') - self.assertTrue(response.data.get('access_granter') is not None, 'access_granter should be returned to owner') + self.assertTrue( + response.data.get("rems_identifier") is not None, + "rems_identifier should be returned to owner", + ) + self.assertTrue( + response.data.get("access_granter") is not None, + "access_granter should be returned to owner", + ) - self._set_http_authorization('no') + self._set_http_authorization("no") response = self.client.get(f'/rest/datasets/{response.data["id"]}') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data.get('rems_identifier') is None, 'rems_identifier should not be returned to Anon') - self.assertTrue(response.data.get('access_granter') is None, 'access_granter should not be returned to Anon') + self.assertTrue( + response.data.get("rems_identifier") is None, + "rems_identifier should not be returned to Anon", + ) + self.assertTrue( + response.data.get("access_granter") is None, + "access_granter should not be returned to Anon", + ) @responses.activate def test_rems_info_cannot_be_changed(self): @@ -4551,10 +5295,18 @@ def test_rems_info_cannot_be_changed(self): cr = response.data - cr['rems_identifier'] = 'some:new:identifier' - cr['access_granter']['name'] = 'New Name' + cr["rems_identifier"] = "some:new:identifier" + cr["access_granter"]["name"] = "New Name" response = self.client.put(f'/rest/datasets/{cr["id"]}', cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertNotEqual(response.data['rems_identifier'], 'some:new:identifier', 'rems_id should not be changed') - self.assertNotEqual(response.data['access_granter'], 'New Name', 'access_granter should not be changed') + self.assertNotEqual( + response.data["rems_identifier"], + "some:new:identifier", + "rems_id should not be changed", + ) + self.assertNotEqual( + response.data["access_granter"], + "New Name", + "access_granter should not be changed", + ) diff --git a/src/metax_api/tests/api/rest/base/views/directories/read.py b/src/metax_api/tests/api/rest/base/views/directories/read.py index 9ebce164..0dff8efc 100755 --- a/src/metax_api/tests/api/rest/base/views/directories/read.py +++ b/src/metax_api/tests/api/rest/base/views/directories/read.py @@ -19,19 +19,18 @@ class DirectoryApiReadCommon(APITestCase, TestClassUtils): - @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(DirectoryApiReadCommon, cls).setUpClass() def setUp(self): - dir_from_test_data = self._get_object_from_test_data('directory') - self.identifier = dir_from_test_data['identifier'] - self.pk = dir_from_test_data['id'] + dir_from_test_data = self._get_object_from_test_data("directory") + self.identifier = dir_from_test_data["identifier"] + self.pk = dir_from_test_data["id"] self._use_http_authorization() def _create_test_dirs(self, count): @@ -39,39 +38,49 @@ def _create_test_dirs(self, count): with transaction.atomic(): for n in range(1, count): f = self._get_new_file_data(str(n)) - self.client.post('/rest/files', f, format="json") + self.client.post("/rest/files", f, format="json") def _get_dirs_files_ids(self, url): file_data = self.client.get(url).data if isinstance(file_data, dict): - return {key: [f['id'] for f in file_data[key]] for key in file_data.keys() - if key in ['directories', 'files']} + return { + key: [f["id"] for f in file_data[key]] + for key in file_data.keys() + if key in ["directories", "files"] + } else: - return [f['id'] for f in file_data] + return [f["id"] for f in file_data] class DirectoryApiReadBasicTests(DirectoryApiReadCommon): - def test_read_directory_list(self): - response = self.client.get('/rest/directories') + response = self.client.get("/rest/directories") self.assertEqual(response.status_code, 501) def test_read_directory_details_by_pk(self): - response = self.client.get('/rest/directories/%s' % self.pk) + response = self.client.get("/rest/directories/%s" % self.pk) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(hasattr(response, 'data'), True, 'Request response object is missing attribute \'data\'') - self.assertEqual('directory_name' in response.data.keys(), True) - self.assertEqual(response.data['identifier'], self.identifier) + self.assertEqual( + hasattr(response, "data"), + True, + "Request response object is missing attribute 'data'", + ) + self.assertEqual("directory_name" in response.data.keys(), True) + self.assertEqual(response.data["identifier"], self.identifier) def test_read_directory_details_by_identifier(self): - response = self.client.get('/rest/directories/%s' % self.identifier) + response = self.client.get("/rest/directories/%s" % self.identifier) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(hasattr(response, 'data'), True, 'Request response object is missing attribute \'data\'') - self.assertEqual('directory_name' in response.data.keys(), True) - self.assertEqual(response.data['identifier'], self.identifier) + self.assertEqual( + hasattr(response, "data"), + True, + "Request response object is missing attribute 'data'", + ) + self.assertEqual("directory_name" in response.data.keys(), True) + self.assertEqual(response.data["identifier"], self.identifier) def test_read_directory_details_not_found(self): - response = self.client.get('/rest/directories/shouldnotexist') + response = self.client.get("/rest/directories/shouldnotexist") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) @@ -85,37 +94,37 @@ def test_read_directory_get_files(self): """ Test browsing files """ - response = self.client.get('/rest/directories/2/files') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data['directories'][0]['id'], 3) - self.assertEqual(response.data['directories'][0]['parent_directory']['id'], 2) - self.assertEqual(len(response.data['files']), 0) - - response = self.client.get('/rest/directories/3/files') - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data['directories'][0]['id'], 4) - self.assertEqual(response.data['directories'][0]['parent_directory']['id'], 3) - self.assertEqual(len(response.data['files']), 5) - self.assertEqual(response.data['files'][0]['parent_directory']['id'], 3) - self.assertEqual(response.data['files'][4]['parent_directory']['id'], 3) - - response = self.client.get('/rest/directories/4/files') - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data['directories'][0]['parent_directory']['id'], 4) - self.assertEqual(len(response.data['files']), 5) - self.assertEqual(response.data['files'][0]['parent_directory']['id'], 4) - self.assertEqual(response.data['files'][4]['parent_directory']['id'], 4) - - response = self.client.get('/rest/directories/5/files') - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['files']), 0) - - response = self.client.get('/rest/directories/6/files') - self.assertEqual(len(response.data['directories']), 0) - self.assertEqual(len(response.data['files']), 10) - self.assertEqual(response.data['files'][0]['parent_directory']['id'], 6) - self.assertEqual(response.data['files'][9]['parent_directory']['id'], 6) + response = self.client.get("/rest/directories/2/files") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(response.data["directories"][0]["id"], 3) + self.assertEqual(response.data["directories"][0]["parent_directory"]["id"], 2) + self.assertEqual(len(response.data["files"]), 0) + + response = self.client.get("/rest/directories/3/files") + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(response.data["directories"][0]["id"], 4) + self.assertEqual(response.data["directories"][0]["parent_directory"]["id"], 3) + self.assertEqual(len(response.data["files"]), 5) + self.assertEqual(response.data["files"][0]["parent_directory"]["id"], 3) + self.assertEqual(response.data["files"][4]["parent_directory"]["id"], 3) + + response = self.client.get("/rest/directories/4/files") + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(response.data["directories"][0]["parent_directory"]["id"], 4) + self.assertEqual(len(response.data["files"]), 5) + self.assertEqual(response.data["files"][0]["parent_directory"]["id"], 4) + self.assertEqual(response.data["files"][4]["parent_directory"]["id"], 4) + + response = self.client.get("/rest/directories/5/files") + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(len(response.data["files"]), 0) + + response = self.client.get("/rest/directories/6/files") + self.assertEqual(len(response.data["directories"]), 0) + self.assertEqual(len(response.data["files"]), 10) + self.assertEqual(response.data["files"][0]["parent_directory"]["id"], 6) + self.assertEqual(response.data["files"][9]["parent_directory"]["id"], 6) def test_read_directory_get_files_recursively(self): """ @@ -123,85 +132,88 @@ def test_read_directory_get_files_recursively(self): """ # without depth, returns from depth=1, which should contain no files - response = self.client.get('/rest/directories/1/files?recursive') + response = self.client.get("/rest/directories/1/files?recursive") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), 0) # dir id 1 (the root) contains 0 files, but recursively 20 - response = self.client.get('/rest/directories/1/files?recursive=true&depth=*') + response = self.client.get("/rest/directories/1/files?recursive=true&depth=*") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 20) # dir id 3 contains 5 files, but recursively 20 - response = self.client.get('/rest/directories/3/files?recursive=true&depth=*') + response = self.client.get("/rest/directories/3/files?recursive=true&depth=*") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 20) # dir id 4 contains 5 files, but recursively 15 - response = self.client.get('/rest/directories/4/files?recursive=true&depth=*') + response = self.client.get("/rest/directories/4/files?recursive=true&depth=*") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 15) # dir id 5 contains 0 files - response = self.client.get('/rest/directories/5/files?recursive=true&depth=*') + response = self.client.get("/rest/directories/5/files?recursive=true&depth=*") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 10) # dir id 6 contains 10 files - response = self.client.get('/rest/directories/6/files?recursive=true&depth=*') + response = self.client.get("/rest/directories/6/files?recursive=true&depth=*") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 10) def test_read_directory_get_files_file_not_found(self): - response = self.client.get('/rest/directories/not_found/files') + response = self.client.get("/rest/directories/not_found/files") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_read_directory_get_project_root_directory(self): - response = self.client.get('/rest/directories/root?project=project_x') + response = self.client.get("/rest/directories/root?project=project_x") self.assertEqual(response.status_code, status.HTTP_200_OK, response.content) - self.assertEqual(response.data['id'], 1) - self.assertEqual('directories' in response.data, True) - self.assertEqual('files' in response.data, True) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data['directories'][0]['id'], 2) + self.assertEqual(response.data["id"], 1) + self.assertEqual("directories" in response.data, True) + self.assertEqual("files" in response.data, True) + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(response.data["directories"][0]["id"], 2) def test_read_directory_get_project_root_directory_not_found(self): - response = self.client.get('/rest/directories/root?project=project_xyz') + response = self.client.get("/rest/directories/root?project=project_xyz") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_read_directory_get_project_root_directory_parameter_missing(self): - response = self.client.get('/rest/directories/root') + response = self.client.get("/rest/directories/root") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('required' in response.data['detail'][0], True, response.data) + self.assertEqual("required" in response.data["detail"][0], True, response.data) def test_read_directory_get_files_by_path(self): dr = Directory.objects.get(pk=2) - response = self.client.get('/rest/directories/files?path=%s&project=%s' % - (dr.directory_path, dr.project_identifier)) + response = self.client.get( + "/rest/directories/files?path=%s&project=%s" + % (dr.directory_path, dr.project_identifier) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data['directories'][0]['id'], 3) - self.assertEqual(response.data['directories'][0]['parent_directory']['id'], 2) - self.assertEqual(len(response.data['files']), 0) + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(response.data["directories"][0]["id"], 3) + self.assertEqual(response.data["directories"][0]["parent_directory"]["id"], 2) + self.assertEqual(len(response.data["files"]), 0) def test_read_directory_get_files_by_path_not_found(self): - response = self.client.get('/rest/directories/files?path=%s&project=%s' % - ('doesnotexist', 'doesnotexist')) + response = self.client.get( + "/rest/directories/files?path=%s&project=%s" % ("doesnotexist", "doesnotexist") + ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_read_directory_get_files_by_path_check_parameters(self): - response = self.client.get('/rest/directories/files') + response = self.client.get("/rest/directories/files") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/directories/files?path=something') + response = self.client.get("/rest/directories/files?path=something") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/directories/files?project=something') + response = self.client.get("/rest/directories/files?project=something") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_read_directory_recursively_with_max_depth(self): """ Should return a flat list of files, three directories deep """ - response = self.client.get('/rest/directories/2/files?recursive=true&depth=3') + response = self.client.get("/rest/directories/2/files?recursive=true&depth=3") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 10) @@ -209,11 +221,13 @@ def test_read_directory_recursively_with_dirs_only_and_max_depth(self): """ Should return a directory hierarchy, three directories deep, with no files at all. """ - response = self.client.get('/rest/directories/2/files?recursive=true&directories_only=true&depth=3') + response = self.client.get( + "/rest/directories/2/files?recursive=true&directories_only=true&depth=3" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('directories' in response.data, True) - self.assertEqual('directories' in response.data['directories'][0], True) - self.assertEqual('directories' in response.data['directories'][0]['directories'][0], True) + self.assertEqual("directories" in response.data, True) + self.assertEqual("directories" in response.data["directories"][0], True) + self.assertEqual("directories" in response.data["directories"][0]["directories"][0], True) def test_read_directory_recursively_with_no_depth(self): """ @@ -222,162 +236,228 @@ def test_read_directory_recursively_with_no_depth(self): Using parameter directories_only=true to easier count the depth. """ - response = self.client.get('/rest/directories/3/files?recursive=true&directories_only=true') + response = self.client.get("/rest/directories/3/files?recursive=true&directories_only=true") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('directories' in response.data, True) - self.assertEqual('directories' in response.data['directories'][0], True) + self.assertEqual("directories" in response.data, True) + self.assertEqual("directories" in response.data["directories"][0], True) def test_read_directory_return_directories_only(self): - response = self.client.get('/rest/directories/3/files?directories_only') + response = self.client.get("/rest/directories/3/files?directories_only") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual('files' in response.data, False) + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual("files" in response.data, False) def test_read_directory_with_include_parent(self): - response = self.client.get('/rest/directories/3/files?include_parent') + response = self.client.get("/rest/directories/3/files?include_parent") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['files']), 5) - self.assertEqual(response.data.get('id', None), 3) + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(len(response.data["files"]), 5) + self.assertEqual(response.data.get("id", None), 3) def test_read_directory_files_sorted_by_file_path(self): - response = self.client.get('/rest/directories/3/files') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['files'][0]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_1') - self.assertEqual(response.data['files'][1]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_2') - self.assertEqual(response.data['files'][2]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_3') - - response = self.client.get('/rest/directories/3/files?pagination&limit=2&offset=2') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['results']['files'][0]['file_path'], - '/project_x_FROZEN/Experiment_X/file_name_2') - self.assertEqual(response.data['results']['files'][1]['file_path'], - '/project_x_FROZEN/Experiment_X/file_name_3') - - response = self.client.get('/rest/directories/3/files?cr_identifier=2') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['files'][0]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_3') - self.assertEqual(response.data['files'][1]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_4') - - response = self.client.get('/rest/directories/3/files?recursive') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data[0]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_1') - self.assertEqual(response.data[1]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_2') - self.assertEqual(response.data[2]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_3') - - response = self.client.get('/rest/directories/3/files?recursive&cr_identifier=2') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data[0]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_3') - self.assertEqual(response.data[1]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_4') + response = self.client.get("/rest/directories/3/files") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["files"][0]["file_path"], + "/project_x_FROZEN/Experiment_X/file_name_1", + ) + self.assertEqual( + response.data["files"][1]["file_path"], + "/project_x_FROZEN/Experiment_X/file_name_2", + ) + self.assertEqual( + response.data["files"][2]["file_path"], + "/project_x_FROZEN/Experiment_X/file_name_3", + ) + + response = self.client.get("/rest/directories/3/files?pagination&limit=2&offset=2") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["results"]["files"][0]["file_path"], + "/project_x_FROZEN/Experiment_X/file_name_2", + ) + self.assertEqual( + response.data["results"]["files"][1]["file_path"], + "/project_x_FROZEN/Experiment_X/file_name_3", + ) + + response = self.client.get("/rest/directories/3/files?cr_identifier=2") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["files"][0]["file_path"], + "/project_x_FROZEN/Experiment_X/file_name_3", + ) + self.assertEqual( + response.data["files"][1]["file_path"], + "/project_x_FROZEN/Experiment_X/file_name_4", + ) + + response = self.client.get("/rest/directories/3/files?recursive") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data[0]["file_path"], "/project_x_FROZEN/Experiment_X/file_name_1" + ) + self.assertEqual( + response.data[1]["file_path"], "/project_x_FROZEN/Experiment_X/file_name_2" + ) + self.assertEqual( + response.data[2]["file_path"], "/project_x_FROZEN/Experiment_X/file_name_3" + ) + + response = self.client.get("/rest/directories/3/files?recursive&cr_identifier=2") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data[0]["file_path"], "/project_x_FROZEN/Experiment_X/file_name_3" + ) + self.assertEqual( + response.data[1]["file_path"], "/project_x_FROZEN/Experiment_X/file_name_4" + ) def test_read_directory_directories_sorted_by_directory_path(self): - response = self.client.get('/rest/directories/8/files') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['directories'][0]['directory_path'], - '/prj_112_root/other') - self.assertEqual(response.data['directories'][1]['directory_path'], - '/prj_112_root/random_folder', response.data) - self.assertEqual(response.data['directories'][2]['directory_path'], - '/prj_112_root/science_data_A') - self.assertEqual(response.data['directories'][3]['directory_path'], - '/prj_112_root/science_data_B') - self.assertEqual(response.data['directories'][4]['directory_path'], - '/prj_112_root/science_data_C') - - response = self.client.get('/rest/directories/8/files?pagination&limit=2&offset=2') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['results']['directories'][0]['directory_path'], - '/prj_112_root/science_data_A') - self.assertEqual(response.data['results']['directories'][1]['directory_path'], - '/prj_112_root/science_data_B') - - response = self.client.get('/rest/directories/8/files?directories_only') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['directories'][0]['directory_path'], - '/prj_112_root/other') - self.assertEqual(response.data['directories'][1]['directory_path'], - '/prj_112_root/random_folder', response.data) - self.assertEqual(response.data['directories'][2]['directory_path'], - '/prj_112_root/science_data_A') - self.assertEqual(response.data['directories'][3]['directory_path'], - '/prj_112_root/science_data_B') - self.assertEqual(response.data['directories'][4]['directory_path'], - '/prj_112_root/science_data_C') - - response = self.client.get('/rest/directories/8/files?cr_identifier=13') + response = self.client.get("/rest/directories/8/files") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["directories"][0]["directory_path"], "/prj_112_root/other") + self.assertEqual( + response.data["directories"][1]["directory_path"], + "/prj_112_root/random_folder", + response.data, + ) + self.assertEqual( + response.data["directories"][2]["directory_path"], + "/prj_112_root/science_data_A", + ) + self.assertEqual( + response.data["directories"][3]["directory_path"], + "/prj_112_root/science_data_B", + ) + self.assertEqual( + response.data["directories"][4]["directory_path"], + "/prj_112_root/science_data_C", + ) + + response = self.client.get("/rest/directories/8/files?pagination&limit=2&offset=2") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["results"]["directories"][0]["directory_path"], + "/prj_112_root/science_data_A", + ) + self.assertEqual( + response.data["results"]["directories"][1]["directory_path"], + "/prj_112_root/science_data_B", + ) + + response = self.client.get("/rest/directories/8/files?directories_only") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["directories"][0]["directory_path"], "/prj_112_root/other") + self.assertEqual( + response.data["directories"][1]["directory_path"], + "/prj_112_root/random_folder", + response.data, + ) + self.assertEqual( + response.data["directories"][2]["directory_path"], + "/prj_112_root/science_data_A", + ) + self.assertEqual( + response.data["directories"][3]["directory_path"], + "/prj_112_root/science_data_B", + ) + self.assertEqual( + response.data["directories"][4]["directory_path"], + "/prj_112_root/science_data_C", + ) + + response = self.client.get("/rest/directories/8/files?cr_identifier=13") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['directories'][0]['directory_path'], - '/prj_112_root/other') - self.assertEqual(response.data['directories'][1]['directory_path'], - '/prj_112_root/random_folder') - self.assertEqual(response.data['directories'][2]['directory_path'], - '/prj_112_root/science_data_A') - self.assertEqual(response.data['directories'][3]['directory_path'], - '/prj_112_root/science_data_B') + self.assertEqual(response.data["directories"][0]["directory_path"], "/prj_112_root/other") + self.assertEqual( + response.data["directories"][1]["directory_path"], + "/prj_112_root/random_folder", + ) + self.assertEqual( + response.data["directories"][2]["directory_path"], + "/prj_112_root/science_data_A", + ) + self.assertEqual( + response.data["directories"][3]["directory_path"], + "/prj_112_root/science_data_B", + ) class DirectoryApiReadFileBrowsingRetrieveSpecificFieldsTests(DirectoryApiReadCommon): - def test_retrieve_requested_directory_fields_only(self): - response = self.client.get('/rest/directories/3/files?directory_fields=identifier,directory_path') + response = self.client.get( + "/rest/directories/3/files?directory_fields=identifier,directory_path" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertEqual('identifier' in response.data['directories'][0], True) - self.assertEqual('directory_path' in response.data['directories'][0], True) + self.assertEqual(len(response.data["directories"][0].keys()), 2) + self.assertEqual("identifier" in response.data["directories"][0], True) + self.assertEqual("directory_path" in response.data["directories"][0], True) - response = self.client.get('/rest/directories/17/files? \ - cr_identifier=13&directory_fields=directory_name&directories_only&recursive') + response = self.client.get( + "/rest/directories/17/files? \ + cr_identifier=13&directory_fields=directory_name&directories_only&recursive" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertTrue('directory_name' in response.data['directories'][0]) - self.assertTrue('directories' in response.data['directories'][0]) - self.assertFalse('id' in response.data['directories'][0]) + self.assertEqual(len(response.data["directories"][0].keys()), 2) + self.assertTrue("directory_name" in response.data["directories"][0]) + self.assertTrue("directories" in response.data["directories"][0]) + self.assertFalse("id" in response.data["directories"][0]) def test_retrieve_directory_byte_size_and_file_count(self): """ There is some additional logic involved in retrieving byte_size and file_count, which warrants targeted tests for just those fields. """ - response = self.client.get('/rest/directories/3/files?directory_fields=identifier,byte_size') + response = self.client.get( + "/rest/directories/3/files?directory_fields=identifier,byte_size" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertEqual('identifier' in response.data['directories'][0], True) - self.assertEqual('byte_size' in response.data['directories'][0], True) + self.assertEqual(len(response.data["directories"][0].keys()), 2) + self.assertEqual("identifier" in response.data["directories"][0], True) + self.assertEqual("byte_size" in response.data["directories"][0], True) - response = self.client.get('/rest/directories/3/files?directory_fields=identifier,file_count') + response = self.client.get( + "/rest/directories/3/files?directory_fields=identifier,file_count" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertEqual('identifier' in response.data['directories'][0], True) - self.assertEqual('file_count' in response.data['directories'][0], True) + self.assertEqual(len(response.data["directories"][0].keys()), 2) + self.assertEqual("identifier" in response.data["directories"][0], True) + self.assertEqual("file_count" in response.data["directories"][0], True) - response = self.client.get('/rest/directories/3/files?directory_fields=identifier,file_count&cr_identifier=3') + response = self.client.get( + "/rest/directories/3/files?directory_fields=identifier,file_count&cr_identifier=3" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertTrue('identifier' in response.data['directories'][0]) - self.assertTrue('file_count' in response.data['directories'][0]) + self.assertEqual(len(response.data["directories"][0].keys()), 2) + self.assertTrue("identifier" in response.data["directories"][0]) + self.assertTrue("file_count" in response.data["directories"][0]) response = self.client.get( - '/rest/directories/3/files?directory_fields=identifier,file_count¬_cr_identifier=2') + "/rest/directories/3/files?directory_fields=identifier,file_count¬_cr_identifier=2" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertTrue('identifier' in response.data['directories'][0]) - self.assertTrue('file_count' in response.data['directories'][0]) + self.assertEqual(len(response.data["directories"][0].keys()), 2) + self.assertTrue("identifier" in response.data["directories"][0]) + self.assertTrue("file_count" in response.data["directories"][0]) def test_retrieve_requested_file_fields_only(self): - response = self.client.get('/rest/directories/3/files?file_fields=identifier,file_path') + response = self.client.get("/rest/directories/3/files?file_fields=identifier,file_path") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['files'][0].keys()), 2) - self.assertEqual('identifier' in response.data['files'][0], True) - self.assertEqual('file_path' in response.data['files'][0], True) + self.assertEqual(len(response.data["files"][0].keys()), 2) + self.assertEqual("identifier" in response.data["files"][0], True) + self.assertEqual("file_path" in response.data["files"][0], True) def test_retrieve_requested_file_and_directory_fields_only(self): - response = self.client.get('/rest/directories/3/files?file_fields=identifier&directory_fields=id') + response = self.client.get( + "/rest/directories/3/files?file_fields=identifier&directory_fields=id" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files'][0].keys()), 1) - self.assertEqual('identifier' in response.data['files'][0], True) - self.assertEqual(len(response.data['directories'][0].keys()), 1) - self.assertEqual('id' in response.data['directories'][0], True) + self.assertEqual(len(response.data["files"][0].keys()), 1) + self.assertEqual("identifier" in response.data["files"][0], True) + self.assertEqual(len(response.data["directories"][0].keys()), 1) + self.assertEqual("id" in response.data["directories"][0], True) def test_not_retrieving_not_allowed_directory_fields(self): from metax_api.api.rest.base.serializers import DirectorySerializer, FileSerializer @@ -385,19 +465,27 @@ def test_not_retrieving_not_allowed_directory_fields(self): allowed_dir_fields = set(DirectorySerializer.Meta.fields) allowed_file_fields = set(FileSerializer.Meta.fields) - response = self.client.get('/rest/directories/3/files?file_fields=parent,id&directory_fields=;;drop db;,id') + response = self.client.get( + "/rest/directories/3/files?file_fields=parent,id&directory_fields=;;drop db;,id" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(any(field in response.data['files'][0].keys() for field in allowed_file_fields)) - self.assertTrue(any(field in response.data['directories'][0].keys() for field in allowed_dir_fields)) + self.assertTrue( + any(field in response.data["files"][0].keys() for field in allowed_file_fields) + ) + self.assertTrue( + any(field in response.data["directories"][0].keys() for field in allowed_dir_fields) + ) - response = self.client.get('/rest/directories/3/files?file_fields=parent&directory_fields=or') + response = self.client.get( + "/rest/directories/3/files?file_fields=parent&directory_fields=or" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/directories/3/files?file_fields=parent') + response = self.client.get("/rest/directories/3/files?file_fields=parent") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/directories/3/files?directory_fields=or') + response = self.client.get("/rest/directories/3/files?directory_fields=or") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -410,50 +498,55 @@ class DirectoryApiReadCatalogRecordFileBrowsingTests(DirectoryApiReadCommon): """ def setUp(self): - self._set_http_authorization('service') - self.client.get('/rest/directories/update_byte_sizes_and_file_counts') - self.client.get('/rest/datasets/update_cr_directory_browsing_data') + self._set_http_authorization("service") + self.client.get("/rest/directories/update_byte_sizes_and_file_counts") + self.client.get("/rest/datasets/update_cr_directory_browsing_data") def test_read_directory_catalog_record_and_not_catalog_record_not_ok(self): """ Test query parameter 'cr_identifier' and 'not_cr_identifier' can not be queried together. """ - response = self.client.get('/rest/directories/3/files?cr_identifier=1¬_cr_identifier=2') + response = self.client.get("/rest/directories/3/files?cr_identifier=1¬_cr_identifier=2") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue("one query parameter of 'cr_identifier' and 'not_cr_identifier'" in response.data['detail'][0]) + self.assertTrue( + "one query parameter of 'cr_identifier' and 'not_cr_identifier'" + in response.data["detail"][0] + ) def test_read_directory_for_catalog_record(self): """ Test query parameter 'cr_identifier'. """ - response = self.client.get('/rest/directories/3/files?cr_identifier=%s' - % CatalogRecord.objects.get(pk=1).identifier) + response = self.client.get( + "/rest/directories/3/files?cr_identifier=%s" + % CatalogRecord.objects.get(pk=1).identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('directories' in response.data, True) - self.assertEqual('files' in response.data, True) - self.assertEqual(len(response.data['directories']), 0) - self.assertEqual(len(response.data['files']), 2) - for f in response.data['files']: - self.assertTrue(f['parent_directory']['id'], 1) + self.assertEqual("directories" in response.data, True) + self.assertEqual("files" in response.data, True) + self.assertEqual(len(response.data["directories"]), 0) + self.assertEqual(len(response.data["files"]), 2) + for f in response.data["files"]: + self.assertTrue(f["parent_directory"]["id"], 1) def test_read_directory_for_not_catalog_record(self): """ Test query parameter 'not_cr_identifier'. """ - response = self.client.get('/rest/directories/3/files?not_cr_identifier=2') + response = self.client.get("/rest/directories/3/files?not_cr_identifier=2") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 3, response.data) - for f in response.data['files']: - self.assertNotEqual(f['parent_directory']['id'], 2, response.data) - self.assertEqual(len(response.data['directories']), 1, response.data) - self.assertNotEqual(response.data['directories'][0]['parent_directory']['id'], 2) + self.assertEqual(len(response.data["files"]), 3, response.data) + for f in response.data["files"]: + self.assertNotEqual(f["parent_directory"]["id"], 2, response.data) + self.assertEqual(len(response.data["directories"]), 1, response.data) + self.assertNotEqual(response.data["directories"][0]["parent_directory"]["id"], 2) def test_read_directory_for_catalog_record_not_found(self): """ Not found cr_identifier should raise 400 instead of 404, which is raised when the directory itself is not found. the error contains details about the 400. """ - response = self.client.get('/rest/directories/3/files?cr_identifier=notexisting') + response = self.client.get("/rest/directories/3/files?cr_identifier=notexisting") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_read_directory_for_not_catalog_record_not_found(self): @@ -461,7 +554,7 @@ def test_read_directory_for_not_catalog_record_not_found(self): Not found cr_identifier should raise 400 instead of 404, which is raised when the directory itself is not found. the error contains details about the 400. """ - response = self.client.get('/rest/directories/3/files?not_cr_identifier=notexisting') + response = self.client.get("/rest/directories/3/files?not_cr_identifier=notexisting") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_read_directory_for_catalog_record_directory_does_not_exist(self): @@ -471,61 +564,69 @@ def test_read_directory_for_catalog_record_directory_does_not_exist(self): """ # should be OK... - response = self.client.get('/rest/directories/4/files') + response = self.client.get("/rest/directories/4/files") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['files']), 5) + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(len(response.data["files"]), 5) # ... but should not contain any files FOR THIS CR - response = self.client.get('/rest/directories/4/files?cr_identifier=1') + response = self.client.get("/rest/directories/4/files?cr_identifier=1") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) # ... and should contain files ALL BUT THIS CR - response = self.client.get('/rest/directories/4/files?not_cr_identifier=1') + response = self.client.get("/rest/directories/4/files?not_cr_identifier=1") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['files']), 5) + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(len(response.data["files"]), 5) def test_read_directory_for_catalog_record_recursively(self): """ Test query parameters 'cr_identifier' with 'recursive'. """ - response = self.client.get('/rest/directories/1/files?recursive&cr_identifier=%s&depth=*' - % CatalogRecord.objects.get(pk=1).identifier) + response = self.client.get( + "/rest/directories/1/files?recursive&cr_identifier=%s&depth=*" + % CatalogRecord.objects.get(pk=1).identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - file_list = list(File.objects.filter(record__pk=1).values_list('id', flat=True)) + file_list = list(File.objects.filter(record__pk=1).values_list("id", flat=True)) self.assertEqual(len(response.data), len(file_list)) for f in response.data: - self.assertTrue(f['id'] in file_list) + self.assertTrue(f["id"] in file_list) - response = self.client.get('/rest/directories/1/files?recursive&cr_identifier=1&depth=*&directories_only') + response = self.client.get( + "/rest/directories/1/files?recursive&cr_identifier=1&depth=*&directories_only" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['directories'][0]['directories']), 1) - self.assertEqual(len(response.data['directories'][0]['directories'][0]['directories']), 0) - self.assertFalse(response.data.get('files')) + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(len(response.data["directories"][0]["directories"]), 1) + self.assertEqual(len(response.data["directories"][0]["directories"][0]["directories"]), 0) + self.assertFalse(response.data.get("files")) # not found cr_identifier should raise 400 instead of 404, which is raised when the # directory itself is not found. the error contains details about the 400 - response = self.client.get('/rest/directories/1/files?recursive&cr_identifier=notexisting') + response = self.client.get("/rest/directories/1/files?recursive&cr_identifier=notexisting") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_read_directory_for_not_catalog_record_recursively(self): """ Test query parameters 'not_cr_identifier' with 'recursive'. """ - file_recursive = self.client.get('/rest/directories/1/files?recursive&depth=*').data - file_list = list(File.objects.filter(record__pk=1).values_list('id', flat=True)) - response = self.client.get('/rest/directories/1/files?recursive&depth=*¬_cr_identifier=%s' - % CatalogRecord.objects.get(pk=1).identifier) + file_recursive = self.client.get("/rest/directories/1/files?recursive&depth=*").data + file_list = list(File.objects.filter(record__pk=1).values_list("id", flat=True)) + response = self.client.get( + "/rest/directories/1/files?recursive&depth=*¬_cr_identifier=%s" + % CatalogRecord.objects.get(pk=1).identifier + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), len(file_recursive) - len(file_list)) for f in response.data: - self.assertTrue(f['id'] not in file_list) + self.assertTrue(f["id"] not in file_list) # not found not_cr_identifier should raise 400 instead of 404, which is raised when the # directory itself is not found. the error contains details about the 400 - response = self.client.get('/rest/directories/1/files?recursive¬_cr_identifier=notexisting') + response = self.client.get( + "/rest/directories/1/files?recursive¬_cr_identifier=notexisting" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_directory_byte_size_and_file_count(self): @@ -533,53 +634,62 @@ def test_directory_byte_size_and_file_count(self): Test byte size and file count are calculated correctly for directories when browsing files in the context of a single record. """ + def _assert_dir_calculations(cr, dr): """ Assert directory numbers received from browsing-api matches what exists in the db when making a reasonably fool-proof query of files by directory path """ - self.assertEqual('byte_size' in dr, True) - self.assertEqual('file_count' in dr, True) + self.assertEqual("byte_size" in dr, True) + self.assertEqual("file_count" in dr, True) - byte_size = cr.files.filter(file_path__startswith='%s/' % dr['directory_path']) \ - .aggregate(Sum('byte_size'))['byte_size__sum'] + byte_size = cr.files.filter( + file_path__startswith="%s/" % dr["directory_path"] + ).aggregate(Sum("byte_size"))["byte_size__sum"] - file_count = cr.files.filter(file_path__startswith='%s/' % dr['directory_path']).count() + file_count = cr.files.filter(file_path__startswith="%s/" % dr["directory_path"]).count() - self.assertEqual(dr['byte_size'], byte_size, 'path: %s' % dr['directory_path']) - self.assertEqual(dr['file_count'], file_count, 'path: %s' % dr['directory_path']) + self.assertEqual(dr["byte_size"], byte_size, "path: %s" % dr["directory_path"]) + self.assertEqual(dr["file_count"], file_count, "path: %s" % dr["directory_path"]) # prepare a new test dataset which contains a directory from testdata, which contains a decent # qty of files and complexity - dr = Directory.objects.get(directory_path='/prj_112_root') + dr = Directory.objects.get(directory_path="/prj_112_root") cr_with_dirs = CatalogRecord.objects.get(pk=13) - cr_data = response = self.client.get('/rest/datasets/1').data - cr_data.pop('id') - cr_data.pop('identifier') - cr_data['research_dataset'].pop('preferred_identifier') - cr_data['research_dataset']['directories'] = [{ - 'identifier': dr.identifier, - 'title': 'test dir', - 'use_category': { - 'identifier': cr_with_dirs.research_dataset['directories'][0]['use_category']['identifier'] + cr_data = response = self.client.get("/rest/datasets/1").data + cr_data.pop("id") + cr_data.pop("identifier") + cr_data["research_dataset"].pop("preferred_identifier") + cr_data["research_dataset"]["directories"] = [ + { + "identifier": dr.identifier, + "title": "test dir", + "use_category": { + "identifier": cr_with_dirs.research_dataset["directories"][0]["use_category"][ + "identifier" + ] + }, } - }] - self._use_http_authorization(username='metax') - cr_data = response = self.client.post('/rest/datasets', cr_data, format='json').data - cr = CatalogRecord.objects.get(pk=cr_data['id']) + ] + self._use_http_authorization(username="metax") + cr_data = response = self.client.post("/rest/datasets", cr_data, format="json").data + cr = CatalogRecord.objects.get(pk=cr_data["id"]) # begin tests # test: browse the file api, and receive a list of sub-directories - response = self.client.get('/rest/directories/%d/files?cr_identifier=%s' % (dr.id, cr.identifier)) + response = self.client.get( + "/rest/directories/%d/files?cr_identifier=%s" % (dr.id, cr.identifier) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - for directory in response.data['directories']: + for directory in response.data["directories"]: _assert_dir_calculations(cr, directory) # test: browse with ?include_parent=true to get the dir directly that was added to the dataset - response = self.client.get('/rest/directories/%d/files?cr_identifier=%s&include_parent' - % (dr.id, cr.identifier)) + response = self.client.get( + "/rest/directories/%d/files?cr_identifier=%s&include_parent" % (dr.id, cr.identifier) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) _assert_dir_calculations(cr, response.data) @@ -587,40 +697,54 @@ def test_directory_byte_size_and_file_count_in_parent_directories(self): cr_id = 13 def _assertDirectoryData(id, parent_data): - response = self.client.get('/rest/directories/%d/files?cr_identifier=%d&include_parent' % (id, cr_id)) + response = self.client.get( + "/rest/directories/%d/files?cr_identifier=%d&include_parent" % (id, cr_id) + ) - self.assertEqual(response.data['byte_size'], parent_data[id][0], response.data['id']) - self.assertEqual(response.data['file_count'], parent_data[id][1]) + self.assertEqual(response.data["byte_size"], parent_data[id][0], response.data["id"]) + self.assertEqual(response.data["file_count"], parent_data[id][1]) - if response.data.get('parent_directory'): - return _assertDirectoryData(response.data['parent_directory']['id'], parent_data) + if response.data.get("parent_directory"): + return _assertDirectoryData(response.data["parent_directory"]["id"], parent_data) def _assertDirectoryData_not_cr_id(id, parent_data): - total_dir_res = self.client.get('/rest/directories/%d' % id) - total_dir_size = (total_dir_res.data.get('byte_size', None), total_dir_res.data.get('file_count', None)) - - response = self.client.get('/rest/directories/%d/files?not_cr_identifier=%d&include_parent' % (id, cr_id)) - if response.data.get('id'): - self.assertEqual(response.data['byte_size'], total_dir_size[0] - parent_data[id][0]) - self.assertEqual(response.data['file_count'], total_dir_size[1] - parent_data[id][1]) - - if response.data.get('parent_directory'): - return _assertDirectoryData_not_cr_id(response.data['parent_directory']['id'], parent_data) + total_dir_res = self.client.get("/rest/directories/%d" % id) + total_dir_size = ( + total_dir_res.data.get("byte_size", None), + total_dir_res.data.get("file_count", None), + ) + + response = self.client.get( + "/rest/directories/%d/files?not_cr_identifier=%d&include_parent" % (id, cr_id) + ) + if response.data.get("id"): + self.assertEqual(response.data["byte_size"], total_dir_size[0] - parent_data[id][0]) + self.assertEqual( + response.data["file_count"], total_dir_size[1] - parent_data[id][1] + ) + + if response.data.get("parent_directory"): + return _assertDirectoryData_not_cr_id( + response.data["parent_directory"]["id"], parent_data + ) def _get_parent_dir_data_for_cr(id): - def _get_parents(pk): pk = Directory.objects.get(pk=pk).parent_directory_id if pk: pks.append(pk) _get_parents(pk) - cr_data = CatalogRecord.objects.get(pk=id).files.order_by('parent_directory_id').values_list( - 'parent_directory_id').annotate(Sum('byte_size'), Count('id')) + cr_data = ( + CatalogRecord.objects.get(pk=id) + .files.order_by("parent_directory_id") + .values_list("parent_directory_id") + .annotate(Sum("byte_size"), Count("id")) + ) grouped_cr_data = {} for i in cr_data: - grouped_cr_data[i[0]] = [ int(i[1]), i[2] ] + grouped_cr_data[i[0]] = [int(i[1]), i[2]] drs = {} for dir in grouped_cr_data.keys(): @@ -639,12 +763,18 @@ def _get_parents(pk): # begin tests - cr = self.client.get('/rest/datasets/%d?fields=research_dataset' % cr_id) + cr = self.client.get("/rest/datasets/%d?fields=research_dataset" % cr_id) - dirs = set([Directory.objects.get(identifier=dr['identifier']).id - for dr in cr.data['research_dataset'].get('directories', [])] + - [File.objects.get(identifier=file['identifier']).parent_directory.id - for file in cr.data['research_dataset'].get('files', [])]) + dirs = set( + [ + Directory.objects.get(identifier=dr["identifier"]).id + for dr in cr.data["research_dataset"].get("directories", []) + ] + + [ + File.objects.get(identifier=file["identifier"]).parent_directory.id + for file in cr.data["research_dataset"].get("files", []) + ] + ) parent_data = _get_parent_dir_data_for_cr(cr_id) @@ -666,28 +796,32 @@ def test_returns_ok_for_open_catalog_record_if_no_authorization(self): # Verify /rest/directories//files?cr_identifier=cr_id returns dir files even without authorization for # open catalog record - self._assert_ok(open_cr_json, 'no') + self._assert_ok(open_cr_json, "no") def test_returns_ok_for_login_catalog_record_if_no_authorization(self): - login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(use_login_access_type=True) + login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details( + use_login_access_type=True + ) # Verify /rest/directories//files?cr_identifier=cr_id returns dir files even without authorization for # login catalog record - self._assert_ok(login_cr_json, 'no') + self._assert_ok(login_cr_json, "no") def test_returns_ok_for_open_catalog_record_if_service_authorization(self): open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() # Verify /rest/directories//files?cr_identifier=cr_id returns dir files with service authorization for # open catalog record - self._assert_ok(open_cr_json, 'service') + self._assert_ok(open_cr_json, "service") def test_returns_ok_for_login_catalog_record_if_service_authorization(self): - login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(use_login_access_type=True) + login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details( + use_login_access_type=True + ) # Verify /rest/directories//files?cr_identifier=cr_id returns dir files with service authorization for # login catalog record - self._assert_ok(login_cr_json, 'service') + self._assert_ok(login_cr_json, "service") @responses.activate def test_returns_ok_for_open_catalog_record_if_owner_authorization(self): @@ -696,7 +830,7 @@ def test_returns_ok_for_open_catalog_record_if_owner_authorization(self): # Verify /rest/directories//files?cr_identifier=cr_id returns dir files with owner authorization for # owner-owned open catalog record - self._assert_ok(open_cr_json, 'owner') + self._assert_ok(open_cr_json, "owner") @responses.activate def test_returns_ok_for_login_catalog_record_if_owner_authorization(self): @@ -705,30 +839,36 @@ def test_returns_ok_for_login_catalog_record_if_owner_authorization(self): # Verify /rest/directories//files?cr_identifier=cr_id returns dir files with owner authorization for # owner-owned login_cr_json catalog record - self._assert_ok(login_cr_json, 'owner') + self._assert_ok(login_cr_json, "owner") def test_returns_ok_for_restricted_catalog_record_if_service_authorization(self): restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() # Verify /rest/directories//files?cr_identifier=cr_id returns dir files with service authorization for # restricted catalog record - self._assert_ok(restricted_cr_json, 'service') + self._assert_ok(restricted_cr_json, "service") @responses.activate def test_returns_ok_for_restricted_catalog_record_if_owner_authorization(self): self.create_end_user_data_catalogs() - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details(True) + restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details( + True + ) # Verify /rest/directories//files?cr_identifier=cr_id returns dir files with owner authorization for # owner-owned restricted catalog record - self._assert_ok(restricted_cr_json, 'owner') + self._assert_ok(restricted_cr_json, "owner") - def test_returns_ok_for_embargoed_catalog_record_if_available_reached_and_no_authorization(self): - available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) + def test_returns_ok_for_embargoed_catalog_record_if_available_reached_and_no_authorization( + self, + ): + available_embargoed_cr_json = ( + self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) + ) # Verify /rest/directories//files?cr_identifier=cr_id returns dir files without authorization # for embargoed catalog record whose embargo date has been reached - self._assert_ok(available_embargoed_cr_json, 'no') + self._assert_ok(available_embargoed_cr_json, "no") # THE FORBIDDEN TESTS @@ -737,39 +877,50 @@ def test_returns_forbidden_for_restricted_catalog_record_if_no_authorization(sel # Verify /rest/directories//files?cr_identifier=cr_id returns forbidden without authorization # for restricted catalog record - self._assert_forbidden(restricted_cr_json, 'no') + self._assert_forbidden(restricted_cr_json, "no") - def test_returns_forbidden_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization(self): - not_available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details( - False) + def test_returns_forbidden_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization( + self, + ): + not_available_embargoed_cr_json = ( + self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(False) + ) # Verify /rest/directories//files?cr_identifier=cr_id returns forbidden without authorization # for embargoed catalog record whose embargo date has not been reached # Deactivate credentials - self._assert_forbidden(not_available_embargoed_cr_json, 'no') + self._assert_forbidden(not_available_embargoed_cr_json, "no") def _assert_forbidden(self, cr_json, credentials_type): - dir_id = cr_json['research_dataset']['directories'][0]['identifier'] - cr_id = cr_json['identifier'] + dir_id = cr_json["research_dataset"]["directories"][0]["identifier"] + cr_id = cr_json["identifier"] self._set_http_authorization(credentials_type) - response = self.client.get('/rest/directories/{0}/files?cr_identifier={1}'.format(dir_id, cr_id)) + response = self.client.get( + "/rest/directories/{0}/files?cr_identifier={1}".format(dir_id, cr_id) + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.get('/rest/directories/{0}/files?not_cr_identifier={1}'.format(dir_id, cr_id)) + response = self.client.get( + "/rest/directories/{0}/files?not_cr_identifier={1}".format(dir_id, cr_id) + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def _assert_ok(self, cr_json, credentials_type): - dir_file_amt = cr_json['research_dataset']['directories'][0]['details']['file_count'] - dir_id = cr_json['research_dataset']['directories'][0]['identifier'] - cr_id = cr_json['identifier'] + dir_file_amt = cr_json["research_dataset"]["directories"][0]["details"]["file_count"] + dir_id = cr_json["research_dataset"]["directories"][0]["identifier"] + cr_id = cr_json["identifier"] self._set_http_authorization(credentials_type) - response = self.client.get('/rest/directories/{0}/files?cr_identifier={1}&recursive&depth=*' - .format(dir_id, cr_id)) + response = self.client.get( + "/rest/directories/{0}/files?cr_identifier={1}&recursive&depth=*".format(dir_id, cr_id) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), dir_file_amt) - response = self.client.get('/rest/directories/{0}/files?not_cr_identifier={1}&recursive&depth=*' - .format(dir_id, cr_id)) + response = self.client.get( + "/rest/directories/{0}/files?not_cr_identifier={1}&recursive&depth=*".format( + dir_id, cr_id + ) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), 0) @@ -798,155 +949,237 @@ class DirectoryApiReadQueryFiltersTogetherTests(DirectoryApiReadCommon): def test_browsing_directories_with_filters(self): # directory filters including directories_only - response = self.client.get('/rest/directories/17/files? \ - directories_only&include_parent&directory_fields=id&directory_name=phase') + response = self.client.get( + "/rest/directories/17/files? \ + directories_only&include_parent&directory_fields=id&directory_name=phase" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # file filters are not suppose to break anything with directories_only - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ directories_only&include_parent&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2') + file_fields=id&file_name=2" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # adds cr_identifier - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ directories_only&include_parent&cr_identifier=13&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2') + file_fields=id&file_name=2" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # adds not_cr_identifier - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ directories_only&include_parent¬_cr_identifier=13&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2') + file_fields=id&file_name=2" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # cr_identifier and not_cr_identifier are NOT suppose to work together - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ directories_only&include_parent&cr_identifier=11¬_cr_identifier=13& \ - directory_fields=id&directory_name=phase&file_fields=id&file_name=2') + directory_fields=id&directory_name=phase&file_fields=id&file_name=2" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # adds pagination - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ directories_only&include_parent&cr_identifier=13&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2&pagination') + file_fields=id&file_name=2&pagination" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # adds recursive - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ directories_only&include_parent&cr_identifier=13&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2&recursive&depth=*') + file_fields=id&file_name=2&recursive&depth=*" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # adds recursive and pagination - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ directories_only&include_parent&cr_identifier=13&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2&recursive&depth=*') + file_fields=id&file_name=2&recursive&depth=*" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) def test_browsing_files_and_directories_with_filters(self): # file filters - response = self.client.get('/rest/directories/17/files? \ - include_parent&file_fields=id&file_name=file') + response = self.client.get( + "/rest/directories/17/files? \ + include_parent&file_fields=id&file_name=file" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # adds directory filters - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ include_parent&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase') + directory_fields=id&directory_name=phase" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # adds cr_identifier - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ include_parent&cr_identifier=13&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase') + directory_fields=id&directory_name=phase" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # adds not_cr_identifier - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ include_parent¬_cr_identifier=13&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase') + directory_fields=id&directory_name=phase" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # cr_identifier and not_cr_identifier are NOT suppose to work together - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ include_parent&cr_identifier=13¬_cr_identifier=11&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase') + directory_fields=id&directory_name=phase" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # adds recursive, directory filters are not suppose to break anything - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ include_parent&cr_identifier=13&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase&recursive&depth=*') + directory_fields=id&directory_name=phase&recursive&depth=*" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # adds pagination - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ include_parent&cr_identifier=13&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase&pagination') + directory_fields=id&directory_name=phase&pagination" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # adds recursive and pagination - response = self.client.get('/rest/directories/17/files? \ + response = self.client.get( + "/rest/directories/17/files? \ include_parent&cr_identifier=13&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase&recursive&depth=*&pagination') + directory_fields=id&directory_name=phase&recursive&depth=*&pagination" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) class DirectoryApiReadCatalogRecordFileBrowsingRetrieveSpecificFieldsTests(DirectoryApiReadCommon): - def setUp(self): super().setUp() CatalogRecord.objects.get(pk=12).calculate_directory_byte_sizes_and_file_counts() def test_retrieve_requested_directory_fields_only(self): - response = self.client.get('/rest/datasets/12?file_details&directory_fields=identifier,directory_path') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['directories'][0]['details'].keys()), 2) - self.assertEqual('identifier' in response.data['research_dataset']['directories'][0]['details'], True) - self.assertEqual('directory_path' in response.data['research_dataset']['directories'][0]['details'], True) + response = self.client.get( + "/rest/datasets/12?file_details&directory_fields=identifier,directory_path" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + len(response.data["research_dataset"]["directories"][0]["details"].keys()), + 2, + ) + self.assertEqual( + "identifier" in response.data["research_dataset"]["directories"][0]["details"], + True, + ) + self.assertEqual( + "directory_path" in response.data["research_dataset"]["directories"][0]["details"], + True, + ) def test_retrieve_directory_byte_size_and_file_count(self): """ There is some additional logic involved in retrieving byte_size and file_count, which warrants targeted tests for just those fields. """ - response = self.client.get('/rest/datasets/12?file_details&directory_fields=identifier,byte_size') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['directories'][0]['details'].keys()), 2) - self.assertEqual('identifier' in response.data['research_dataset']['directories'][0]['details'], True) - self.assertEqual('byte_size' in response.data['research_dataset']['directories'][0]['details'], True) + response = self.client.get( + "/rest/datasets/12?file_details&directory_fields=identifier,byte_size" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + len(response.data["research_dataset"]["directories"][0]["details"].keys()), + 2, + ) + self.assertEqual( + "identifier" in response.data["research_dataset"]["directories"][0]["details"], + True, + ) + self.assertEqual( + "byte_size" in response.data["research_dataset"]["directories"][0]["details"], + True, + ) - response = self.client.get('/rest/datasets/12?file_details&directory_fields=identifier,file_count') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['directories'][0]['details'].keys()), 2) - self.assertEqual('identifier' in response.data['research_dataset']['directories'][0]['details'], True) - self.assertEqual('file_count' in response.data['research_dataset']['directories'][0]['details'], True) + response = self.client.get( + "/rest/datasets/12?file_details&directory_fields=identifier,file_count" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + len(response.data["research_dataset"]["directories"][0]["details"].keys()), + 2, + ) + self.assertEqual( + "identifier" in response.data["research_dataset"]["directories"][0]["details"], + True, + ) + self.assertEqual( + "file_count" in response.data["research_dataset"]["directories"][0]["details"], + True, + ) def test_retrieve_requested_file_fields_only(self): - response = self.client.get('/rest/datasets/12?file_details&file_fields=identifier,file_path') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['files'][0]['details'].keys()), 2) - self.assertEqual('identifier' in response.data['research_dataset']['files'][0]['details'], True) - self.assertEqual('file_path' in response.data['research_dataset']['files'][0]['details'], True) + response = self.client.get( + "/rest/datasets/12?file_details&file_fields=identifier,file_path" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["research_dataset"]["files"][0]["details"].keys()), 2) + self.assertEqual( + "identifier" in response.data["research_dataset"]["files"][0]["details"], + True, + ) + self.assertEqual( + "file_path" in response.data["research_dataset"]["files"][0]["details"], + True, + ) def test_retrieve_requested_file_and_directory_fields_only(self): - response = self.client.get('/rest/datasets/12?file_details&file_fields=identifier&directory_fields=id') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['files'][0]['details'].keys()), 1) - self.assertEqual('identifier' in response.data['research_dataset']['files'][0]['details'], True) - self.assertEqual(len(response.data['research_dataset']['directories'][0]['details'].keys()), 1) - self.assertEqual('id' in response.data['research_dataset']['directories'][0]['details'], True) + response = self.client.get( + "/rest/datasets/12?file_details&file_fields=identifier&directory_fields=id" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["research_dataset"]["files"][0]["details"].keys()), 1) + self.assertEqual( + "identifier" in response.data["research_dataset"]["files"][0]["details"], + True, + ) + self.assertEqual( + len(response.data["research_dataset"]["directories"][0]["details"].keys()), + 1, + ) + self.assertEqual( + "id" in response.data["research_dataset"]["directories"][0]["details"], True + ) class DirectoryApiReadEndUserAccess(DirectoryApiReadCommon): - ''' + """ Test End User Access permissions when browsing files using /rest/directories api. Note: In these tests, the token by default does not have correct project groups. Token project groups are only made valid by calling _update_token_with_project_of_directory(). - ''' + """ def setUp(self): super().setUp() @@ -955,76 +1188,92 @@ def setUp(self): def _update_token_with_project_of_directory(self, dir_id): proj = Directory.objects.get(pk=dir_id).project_identifier - self.token['group_names'].append('IDA01:%s' % proj) - self._use_http_authorization(method='bearer', token=self.token) + self.token["group_names"].append("IDA01:%s" % proj) + self._use_http_authorization(method="bearer", token=self.token) @responses.activate def test_user_can_browse_files_from_their_projects(self): - ''' + """ Ensure users can only read files from /rest/directories owned by them. - ''' - self._use_http_authorization(method='bearer', token=self.token) + """ + self._use_http_authorization(method="bearer", token=self.token) # first read files without project access - should fail - response = self.client.get('/rest/directories/1') + response = self.client.get("/rest/directories/1") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - response = self.client.get('/rest/directories/1/files') + response = self.client.get("/rest/directories/1/files") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) # set user to same project as previous files and try again. should now succeed self._update_token_with_project_of_directory(1) - response = self.client.get('/rest/directories/1') + response = self.client.get("/rest/directories/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/directories/1/files') + response = self.client.get("/rest/directories/1/files") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) @responses.activate def test_browsing_by_project_and_file_path_is_protected(self): - self._use_http_authorization(method='bearer', token=self.token) + self._use_http_authorization(method="bearer", token=self.token) dr = Directory.objects.get(pk=2) - response = self.client.get('/rest/directories/files?path=%s&project=%s' % - (dr.directory_path, dr.project_identifier)) + response = self.client.get( + "/rest/directories/files?path=%s&project=%s" + % (dr.directory_path, dr.project_identifier) + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) self._update_token_with_project_of_directory(2) - response = self.client.get('/rest/directories/files?path=%s&project=%s' % - (dr.directory_path, dr.project_identifier)) + response = self.client.get( + "/rest/directories/files?path=%s&project=%s" + % (dr.directory_path, dr.project_identifier) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) @responses.activate def test_browsing_in_cr_context(self): - ''' + """ Cr with open access type should be available for any end-user api user. Browsing files for a cr with restricted access type should be forbidden for non-owner (or service) user. - ''' + """ cr = CatalogRecord.objects.get(pk=1) - self._use_http_authorization(method='bearer', token=self.token) - response = self.client.get('/rest/directories/3/files?cr_identifier={0}'.format(cr.identifier)) + self._use_http_authorization(method="bearer", token=self.token) + response = self.client.get( + "/rest/directories/3/files?cr_identifier={0}".format(cr.identifier) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - cr.research_dataset['access_rights']['access_type']['identifier'] = ACCESS_TYPES['restricted'] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = ACCESS_TYPES[ + "restricted" + ] cr.force_save() - response = self.client.get('/rest/directories/3/files?cr_identifier={0}'.format(cr.identifier)) + response = self.client.get( + "/rest/directories/3/files?cr_identifier={0}".format(cr.identifier) + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @responses.activate def test_browsing_in_not_cr_context(self): - ''' + """ Cr with open access type should be available for any end-user api user. Browsing files for a cr with restricted access type should be forbidden for non-owner (or service) user. - ''' + """ cr = CatalogRecord.objects.get(pk=1) - self._use_http_authorization(method='bearer', token=self.token) - response = self.client.get('/rest/directories/3/files?not_cr_identifier={0}'.format(cr.identifier)) + self._use_http_authorization(method="bearer", token=self.token) + response = self.client.get( + "/rest/directories/3/files?not_cr_identifier={0}".format(cr.identifier) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - cr.research_dataset['access_rights']['access_type']['identifier'] = ACCESS_TYPES['restricted'] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = ACCESS_TYPES[ + "restricted" + ] cr.force_save() - response = self.client.get('/rest/directories/3/files?not_cr_identifier={0}'.format(cr.identifier)) + response = self.client.get( + "/rest/directories/3/files?not_cr_identifier={0}".format(cr.identifier) + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) @@ -1043,149 +1292,193 @@ def test_read_directory_with_default_limit_pagination(self): """ Test browsing files with pagination """ - file_dict = self._get_dirs_files_ids('/rest/directories/24/files') - - response = self.client.get('/rest/directories/24/files?pagination') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 10) - self.assertEqual(len(response.data['results']['files']), 0) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][0]) - self.assertEqual(response.data['results']['directories'][9]['id'], file_dict['directories'][9]) - - next_link = response.data['next'].split('http://testserver')[1] + file_dict = self._get_dirs_files_ids("/rest/directories/24/files") + + response = self.client.get("/rest/directories/24/files?pagination") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]["directories"]), 10) + self.assertEqual(len(response.data["results"]["files"]), 0) + self.assertEqual( + response.data["results"]["directories"][0]["id"], + file_dict["directories"][0], + ) + self.assertEqual( + response.data["results"]["directories"][9]["id"], + file_dict["directories"][9], + ) + + next_link = response.data["next"].split("http://testserver")[1] response = self.client.get(next_link) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 4) - self.assertEqual(len(response.data['results']['files']), 6) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][10]) - self.assertEqual(response.data['results']['directories'][3]['id'], file_dict['directories'][13]) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][0]) - self.assertEqual(response.data['results']['files'][5]['id'], file_dict['files'][5]) - - next_link = response.data['next'].split('http://testserver')[1] + self.assertEqual(len(response.data["results"]["directories"]), 4) + self.assertEqual(len(response.data["results"]["files"]), 6) + self.assertEqual( + response.data["results"]["directories"][0]["id"], + file_dict["directories"][10], + ) + self.assertEqual( + response.data["results"]["directories"][3]["id"], + file_dict["directories"][13], + ) + self.assertEqual(response.data["results"]["files"][0]["id"], file_dict["files"][0]) + self.assertEqual(response.data["results"]["files"][5]["id"], file_dict["files"][5]) + + next_link = response.data["next"].split("http://testserver")[1] response = self.client.get(next_link) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 0) - self.assertEqual(len(response.data['results']['files']), 10) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][6]) - self.assertEqual(response.data['results']['files'][9]['id'], file_dict['files'][15]) + self.assertEqual(len(response.data["results"]["directories"]), 0) + self.assertEqual(len(response.data["results"]["files"]), 10) + self.assertEqual(response.data["results"]["files"][0]["id"], file_dict["files"][6]) + self.assertEqual(response.data["results"]["files"][9]["id"], file_dict["files"][15]) - prev_link = response.data['previous'].split('http://testserver')[1] + prev_link = response.data["previous"].split("http://testserver")[1] response = self.client.get(prev_link) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 4) - self.assertEqual(len(response.data['results']['files']), 6) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][10]) - self.assertEqual(response.data['results']['directories'][3]['id'], file_dict['directories'][13]) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][0]) - self.assertEqual(response.data['results']['files'][5]['id'], file_dict['files'][5]) + self.assertEqual(len(response.data["results"]["directories"]), 4) + self.assertEqual(len(response.data["results"]["files"]), 6) + self.assertEqual( + response.data["results"]["directories"][0]["id"], + file_dict["directories"][10], + ) + self.assertEqual( + response.data["results"]["directories"][3]["id"], + file_dict["directories"][13], + ) + self.assertEqual(response.data["results"]["files"][0]["id"], file_dict["files"][0]) + self.assertEqual(response.data["results"]["files"][5]["id"], file_dict["files"][5]) def test_read_directory_with_custom_limit_pagination(self): - file_dict = self._get_dirs_files_ids('/rest/directories/24/files') - - response = self.client.get('/rest/directories/24/files?limit=4&offset=12&pagination') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 2) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][12]) - self.assertEqual(response.data['results']['directories'][1]['id'], file_dict['directories'][13]) - self.assertEqual(len(response.data['results']['files']), 2) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][0]) - self.assertEqual(response.data['results']['files'][1]['id'], file_dict['files'][1]) - - next_link = response.data['next'].split('http://testserver')[1] - prev_link = response.data['previous'].split('http://testserver')[1] + file_dict = self._get_dirs_files_ids("/rest/directories/24/files") + + response = self.client.get("/rest/directories/24/files?limit=4&offset=12&pagination") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]["directories"]), 2) + self.assertEqual( + response.data["results"]["directories"][0]["id"], + file_dict["directories"][12], + ) + self.assertEqual( + response.data["results"]["directories"][1]["id"], + file_dict["directories"][13], + ) + self.assertEqual(len(response.data["results"]["files"]), 2) + self.assertEqual(response.data["results"]["files"][0]["id"], file_dict["files"][0]) + self.assertEqual(response.data["results"]["files"][1]["id"], file_dict["files"][1]) + + next_link = response.data["next"].split("http://testserver")[1] + prev_link = response.data["previous"].split("http://testserver")[1] response = self.client.get(next_link) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 0) - self.assertEqual(len(response.data['results']['files']), 4) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][2]) - self.assertEqual(response.data['results']['files'][3]['id'], file_dict['files'][5]) + self.assertEqual(len(response.data["results"]["directories"]), 0) + self.assertEqual(len(response.data["results"]["files"]), 4) + self.assertEqual(response.data["results"]["files"][0]["id"], file_dict["files"][2]) + self.assertEqual(response.data["results"]["files"][3]["id"], file_dict["files"][5]) response = self.client.get(prev_link) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 4) - self.assertEqual(len(response.data['results']['files']), 0) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][8]) - self.assertEqual(response.data['results']['directories'][3]['id'], file_dict['directories'][11]) + self.assertEqual(len(response.data["results"]["directories"]), 4) + self.assertEqual(len(response.data["results"]["files"]), 0) + self.assertEqual( + response.data["results"]["directories"][0]["id"], + file_dict["directories"][8], + ) + self.assertEqual( + response.data["results"]["directories"][3]["id"], + file_dict["directories"][11], + ) def test_read_directory_with_recursive_and_pagination(self): - ''' + """ Query with recursive flag must return only files as a list - ''' - file_list = self._get_dirs_files_ids('/rest/directories/24/files?recursive') + """ + file_list = self._get_dirs_files_ids("/rest/directories/24/files?recursive") - response = self.client.get('/rest/directories/24/files?recursive&pagination') + response = self.client.get("/rest/directories/24/files?recursive&pagination") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 10) - self.assertEqual(response.data['results'][0]['id'], file_list[0]) - self.assertEqual(response.data['results'][9]['id'], file_list[9]) + self.assertEqual(len(response.data["results"]), 10) + self.assertEqual(response.data["results"][0]["id"], file_list[0]) + self.assertEqual(response.data["results"][9]["id"], file_list[9]) - next_link = response.data['next'].split('http://testserver')[1] + next_link = response.data["next"].split("http://testserver")[1] response = self.client.get(next_link) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 10) - self.assertEqual(response.data['results'][0]['id'], file_list[10]) - self.assertEqual(response.data['results'][9]['id'], file_list[19]) + self.assertEqual(len(response.data["results"]), 10) + self.assertEqual(response.data["results"][0]["id"], file_list[10]) + self.assertEqual(response.data["results"][9]["id"], file_list[19]) - prev_link = response.data['previous'].split('http://testserver')[1] + prev_link = response.data["previous"].split("http://testserver")[1] response = self.client.get(prev_link) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 10) - self.assertEqual(response.data['results'][0]['id'], file_list[0]) - self.assertEqual(response.data['results'][9]['id'], file_list[9]) + self.assertEqual(len(response.data["results"]), 10) + self.assertEqual(response.data["results"][0]["id"], file_list[0]) + self.assertEqual(response.data["results"][9]["id"], file_list[9]) def test_read_directory_with_dirs_only_and_pagination(self): - ''' + """ Query with directories_only flag must return only directories - ''' - file_dict = self._get_dirs_files_ids('/rest/directories/24/files?directories_only')['directories'] + """ + file_dict = self._get_dirs_files_ids("/rest/directories/24/files?directories_only")[ + "directories" + ] - response = self.client.get('/rest/directories/24/files?directories_only&pagination=true') + response = self.client.get("/rest/directories/24/files?directories_only&pagination=true") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['results']['directories']), 10) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict[0]) - self.assertEqual(response.data['results']['directories'][9]['id'], file_dict[9]) + self.assertEqual(len(response.data["results"]["directories"]), 10) + self.assertEqual(response.data["results"]["directories"][0]["id"], file_dict[0]) + self.assertEqual(response.data["results"]["directories"][9]["id"], file_dict[9]) - next_link = response.data['next'].split('http://testserver')[1] + next_link = response.data["next"].split("http://testserver")[1] response = self.client.get(next_link) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 4) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict[10]) - self.assertEqual(response.data['results']['directories'][3]['id'], file_dict[13]) + self.assertEqual(len(response.data["results"]["directories"]), 4) + self.assertEqual(response.data["results"]["directories"][0]["id"], file_dict[10]) + self.assertEqual(response.data["results"]["directories"][3]["id"], file_dict[13]) - prev_link = response.data['previous'].split('http://testserver')[1] + prev_link = response.data["previous"].split("http://testserver")[1] response = self.client.get(prev_link) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 10) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict[0]) - self.assertEqual(response.data['results']['directories'][9]['id'], file_dict[9]) + self.assertEqual(len(response.data["results"]["directories"]), 10) + self.assertEqual(response.data["results"]["directories"][0]["id"], file_dict[0]) + self.assertEqual(response.data["results"]["directories"][9]["id"], file_dict[9]) def test_read_directory_with_parent_and_pagination(self): - ''' + """ Query with directories_only flag must return only directories - ''' - file_dict = self._get_dirs_files_ids('/rest/directories/24/files?include_parent') - - response = self.client.get('/rest/directories/24/files?include_parent&pagination=true') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 10) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][0]) - self.assertEqual(response.data['results']['directories'][9]['id'], file_dict['directories'][9]) - self.assertEqual(response.data['results']['id'], 24) - self.assertEqual(response.data['results']['directory_name'], "10") - - next_link = response.data['next'].split('http://testserver')[1] + """ + file_dict = self._get_dirs_files_ids("/rest/directories/24/files?include_parent") + + response = self.client.get("/rest/directories/24/files?include_parent&pagination=true") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]["directories"]), 10) + self.assertEqual( + response.data["results"]["directories"][0]["id"], + file_dict["directories"][0], + ) + self.assertEqual( + response.data["results"]["directories"][9]["id"], + file_dict["directories"][9], + ) + self.assertEqual(response.data["results"]["id"], 24) + self.assertEqual(response.data["results"]["directory_name"], "10") + + next_link = response.data["next"].split("http://testserver")[1] response = self.client.get(next_link) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 4) - self.assertEqual(len(response.data['results']['files']), 6) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][10]) - self.assertEqual(response.data['results']['directories'][3]['id'], file_dict['directories'][13]) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][0]) - self.assertEqual(response.data['results']['files'][5]['id'], file_dict['files'][5]) - self.assertEqual(response.data['results']['id'], 24) - self.assertEqual(response.data['results']['directory_name'], "10") + self.assertEqual(len(response.data["results"]["directories"]), 4) + self.assertEqual(len(response.data["results"]["files"]), 6) + self.assertEqual( + response.data["results"]["directories"][0]["id"], + file_dict["directories"][10], + ) + self.assertEqual( + response.data["results"]["directories"][3]["id"], + file_dict["directories"][13], + ) + self.assertEqual(response.data["results"]["files"][0]["id"], file_dict["files"][0]) + self.assertEqual(response.data["results"]["files"][5]["id"], file_dict["files"][5]) + self.assertEqual(response.data["results"]["id"], 24) + self.assertEqual(response.data["results"]["directory_name"], "10") class DirectoryApiReadFileNameDirectoryNameTests(DirectoryApiReadCommon): @@ -1200,144 +1493,172 @@ def setUp(self): def test_browsing_directory_with_file_name(self): - response = self.client.get('/rest/directories/24/files?file_name=') + response = self.client.get("/rest/directories/24/files?file_name=") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['files']), 51) + self.assertEqual(len(response.data["files"]), 51) - response = self.client.get('/rest/directories/24/files?file_name=_name_1') + response = self.client.get("/rest/directories/24/files?file_name=_name_1") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['files']), 21) + self.assertEqual(len(response.data["files"]), 21) - response = self.client.get('/rest/directories/24/files?file_name=0') + response = self.client.get("/rest/directories/24/files?file_name=0") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['files']), 15) + self.assertEqual(len(response.data["files"]), 15) - response = self.client.get('/rest/directories/24/files?file_name=_name_118') + response = self.client.get("/rest/directories/24/files?file_name=_name_118") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['files']), 1) + self.assertEqual(len(response.data["files"]), 1) def test_browsing_directory_with_directory_name(self): - response = self.client.get('/rest/directories/24/files?directory_name=') + response = self.client.get("/rest/directories/24/files?directory_name=") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 5) + self.assertEqual(len(response.data["directories"]), 5) - response = self.client.get('/rest/directories/24/files?directory_name=dir_1') + response = self.client.get("/rest/directories/24/files?directory_name=dir_1") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) + self.assertEqual(len(response.data["directories"]), 1) - response = self.client.get('/rest/directories/24/files?directory_name=dir') + response = self.client.get("/rest/directories/24/files?directory_name=dir") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 5) + self.assertEqual(len(response.data["directories"]), 5) def test_browsing_directory_with_directory_and_file_name(self): - response = self.client.get('/rest/directories/24/files?directory_name=&file_name=') + response = self.client.get("/rest/directories/24/files?directory_name=&file_name=") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 5) - self.assertEqual(len(response.data['files']), 51) + self.assertEqual(len(response.data["directories"]), 5) + self.assertEqual(len(response.data["files"]), 51) - response = self.client.get('/rest/directories/24/files?directory_name=dir_1&file_name=file_name_120') + response = self.client.get( + "/rest/directories/24/files?directory_name=dir_1&file_name=file_name_120" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['files']), 1) + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(len(response.data["files"]), 1) - response = self.client.get('/rest/directories/24/files?directory_name=dir&file_name=not_existing') + response = self.client.get( + "/rest/directories/24/files?directory_name=dir&file_name=not_existing" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 5) - self.assertEqual(len(response.data['files']), 0) + self.assertEqual(len(response.data["directories"]), 5) + self.assertEqual(len(response.data["files"]), 0) def test_browsing_directory_with_file_and_dir_name_and_pagination(self): # second page should return last filtered files - response = self.client.get('/rest/directories/24/files?file_name=0&pagination&limit=10&offset=10') + response = self.client.get( + "/rest/directories/24/files?file_name=0&pagination&limit=10&offset=10" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 0) - self.assertEqual(len(response.data['results']['files']), 10) + self.assertEqual(len(response.data["results"]["directories"]), 0) + self.assertEqual(len(response.data["results"]["files"]), 10) # first page with limit of 3 should return first filtered directories - response = self.client.get('/rest/directories/24/files?directory_name=dir_&pagination&limit=3') + response = self.client.get( + "/rest/directories/24/files?directory_name=dir_&pagination&limit=3" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 3) - self.assertEqual(len(response.data['results']['files']), 0) + self.assertEqual(len(response.data["results"]["directories"]), 3) + self.assertEqual(len(response.data["results"]["files"]), 0) # first page with limit of 3 should return first filtered directories - response = self.client.get('/rest/directories/24/files?directory_name=dir_1&file_name=0&pagination') + response = self.client.get( + "/rest/directories/24/files?directory_name=dir_1&file_name=0&pagination" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 1) - self.assertEqual(len(response.data['results']['files']), 9) + self.assertEqual(len(response.data["results"]["directories"]), 1) + self.assertEqual(len(response.data["results"]["files"]), 9) def test_browsing_directory_with_directory_and_file_name_and_dirs_only(self): - response = self.client.get('/rest/directories/24/files?file_name=_name_11&directories_only') + response = self.client.get("/rest/directories/24/files?file_name=_name_11&directories_only") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 5) - self.assertEqual(response.data.get('files'), None) + self.assertEqual(len(response.data["directories"]), 5) + self.assertEqual(response.data.get("files"), None) - response = self.client.get('/rest/directories/24/files?directory_name=dir_5&directories_only') + response = self.client.get( + "/rest/directories/24/files?directory_name=dir_5&directories_only" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data.get('files'), None) + self.assertEqual(len(response.data["directories"]), 1) + self.assertEqual(response.data.get("files"), None) def test_browsing_directory_with_directory_and_file_name_and_recursive(self): - response = self.client.get('/rest/directories/24/files?file_name=_name_11&recursive') + response = self.client.get("/rest/directories/24/files?file_name=_name_11&recursive") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 10) # should have one file from directory with the rest of filtered files - response = self.client.get('/rest/directories/24/files?directory_name=dir_5&file_name=5&recursive&depth=*') + response = self.client.get( + "/rest/directories/24/files?directory_name=dir_5&file_name=5&recursive&depth=*" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 6) - def test_browsing_directory_with_directory_and_file_name_and_cr_identifier_and_not_cr_identifier(self): + def test_browsing_directory_with_directory_and_file_name_and_cr_identifier_and_not_cr_identifier( + self, + ): # tests for directory_name and cr_identifier - response = self.client.get('/rest/directories/17/files?directory_name=2&cr_identifier=13') + response = self.client.get("/rest/directories/17/files?directory_name=2&cr_identifier=13") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 1) + self.assertEqual(len(response.data["directories"]), 1) - response = self.client.get('/rest/directories/17/files?directory_name=phase&cr_identifier=13') + response = self.client.get( + "/rest/directories/17/files?directory_name=phase&cr_identifier=13" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 2) + self.assertEqual(len(response.data["directories"]), 2) - response = self.client.get('/rest/directories/17/files?directory_name=&cr_identifier=13') + response = self.client.get("/rest/directories/17/files?directory_name=&cr_identifier=13") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 2) + self.assertEqual(len(response.data["directories"]), 2) # tests for directory_name and not_cr_identifier - response = self.client.get('/rest/directories/17/files?directory_name=phase¬_cr_identifier=13') + response = self.client.get( + "/rest/directories/17/files?directory_name=phase¬_cr_identifier=13" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 0) + self.assertEqual(len(response.data["directories"]), 0) - response = self.client.get('/rest/directories/17/files?directory_name=2¬_cr_identifier=13') + response = self.client.get( + "/rest/directories/17/files?directory_name=2¬_cr_identifier=13" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 0) + self.assertEqual(len(response.data["directories"]), 0) # tests for file_name and cr_identifier - response = self.client.get('/rest/directories/12/files?file_name=22&cr_identifier=13') + response = self.client.get("/rest/directories/12/files?file_name=22&cr_identifier=13") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 1) + self.assertEqual(len(response.data["files"]), 1) - response = self.client.get('/rest/directories/12/files?file_name=name_&cr_identifier=13') + response = self.client.get("/rest/directories/12/files?file_name=name_&cr_identifier=13") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 3) + self.assertEqual(len(response.data["files"]), 3) - response = self.client.get('/rest/directories/12/files?file_name=&cr_identifier=13') + response = self.client.get("/rest/directories/12/files?file_name=&cr_identifier=13") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 3) + self.assertEqual(len(response.data["files"]), 3) - response = self.client.get('/rest/directories/17/files?file_name=&cr_identifier=13&directories_only') + response = self.client.get( + "/rest/directories/17/files?file_name=&cr_identifier=13&directories_only" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('files'), None) + self.assertEqual(response.data.get("files"), None) # tests for file_name and not_cr_identifier - response = self.client.get('/rest/directories/16/files?file_name=name¬_cr_identifier=13') + response = self.client.get("/rest/directories/16/files?file_name=name¬_cr_identifier=13") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 1) + self.assertEqual(len(response.data["files"]), 1) - response = self.client.get('/rest/directories/16/files?file_name=name_2¬_cr_identifier=13') + response = self.client.get( + "/rest/directories/16/files?file_name=name_2¬_cr_identifier=13" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 1) + self.assertEqual(len(response.data["files"]), 1) - response = self.client.get('/rest/directories/16/files?file_name=name_1¬_cr_identifier=13') + response = self.client.get( + "/rest/directories/16/files?file_name=name_1¬_cr_identifier=13" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 0) + self.assertEqual(len(response.data["files"]), 0) diff --git a/src/metax_api/tests/api/rest/base/views/directories/write.py b/src/metax_api/tests/api/rest/base/views/directories/write.py index f363a6d2..c9268578 100755 --- a/src/metax_api/tests/api/rest/base/views/directories/write.py +++ b/src/metax_api/tests/api/rest/base/views/directories/write.py @@ -18,17 +18,17 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(DirectoryApiWriteCommon, cls).setUpClass() def setUp(self): """ Reloaded for every test case """ - call_command('loaddata', test_data_file_path, verbosity=0) - dir_from_test_data = self._get_object_from_test_data('directory') - self.identifier = dir_from_test_data['identifier'] - self.directory_name = dir_from_test_data['directory_name'] + call_command("loaddata", test_data_file_path, verbosity=0) + dir_from_test_data = self._get_object_from_test_data("directory") + self.identifier = dir_from_test_data["identifier"] + self.directory_name = dir_from_test_data["directory_name"] """ New data that is sent to the server for POST, PUT, PATCH requests. Modified @@ -39,162 +39,219 @@ def setUp(self): self._use_http_authorization() def _get_new_test_data(self): - from_test_data = self._get_object_from_test_data('directory', requested_index=0) - from_test_data.update({ - "identifier": "urn:nbn:fi:csc-ida201401200000000001", - }) + from_test_data = self._get_object_from_test_data("directory", requested_index=0) + from_test_data.update( + { + "identifier": "urn:nbn:fi:csc-ida201401200000000001", + } + ) return from_test_data def _get_second_new_test_data(self): from_test_data = self._get_new_test_data() - from_test_data.update({ - "identifier": "urn:nbn:fi:csc-ida201401200000000002", - }) + from_test_data.update( + { + "identifier": "urn:nbn:fi:csc-ida201401200000000002", + } + ) return from_test_data class DirectoryApiWriteTests(DirectoryApiWriteCommon): - def test_create_files_for_catalog_record(self): """ Tests flow of creating files and assigning them to dataset. """ - project = 'project-test-files' - directory_path = '/dir/' + project = "project-test-files" + directory_path = "/dir/" files = [] for n in range(1, 4): - file_path = directory_path + 'file' + str(n) - f = self._get_new_file_data(str(n), project=project, file_path=file_path, - directory_path=directory_path, open_access=True) - f.pop('parent_directory', None) + file_path = directory_path + "file" + str(n) + f = self._get_new_file_data( + str(n), + project=project, + file_path=file_path, + directory_path=directory_path, + open_access=True, + ) + f.pop("parent_directory", None) files.append(f) cr = self._get_ida_dataset_without_files() - cr['cumulative_state'] = 1 + cr["cumulative_state"] = 1 - fields = 'file_fields=id,identifier,file_path&directory_fields=id,identifier,directory_path,file_count' + fields = "file_fields=id,identifier,file_path&directory_fields=id,identifier,directory_path,file_count" # start test # - self._set_http_authorization('service') + self._set_http_authorization("service") # adding file1 to /dir/ - response = self.client.post('/rest/files', files[0], format='json') + response = self.client.post("/rest/files", files[0], format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - file1_id = response.data['identifier'] - file1_title = response.data['file_name'] + file1_id = response.data["identifier"] + file1_title = response.data["file_name"] # adding file2 to /dir/ - response = self.client.post('/rest/files', files[1], format='json') + response = self.client.post("/rest/files", files[1], format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - file2_id = response.data['identifier'] - file2_title = response.data['file_name'] + file2_id = response.data["identifier"] + file2_title = response.data["file_name"] # adding file3 to /dir/ - response = self.client.post('/rest/files', files[2], format='json') + response = self.client.post("/rest/files", files[2], format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - file3_id = response.data['identifier'] - file3_title = response.data['file_name'] + file3_id = response.data["identifier"] + file3_title = response.data["file_name"] # creating dataset - response = self.client.post('/rest/datasets', cr, format='json') + response = self.client.post("/rest/datasets", cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] - cr_rd_identifier = response.data['research_dataset']['preferred_identifier'] + cr_id = response.data["id"] + cr_rd_identifier = response.data["research_dataset"]["preferred_identifier"] # getting dataset root directory identifier (%2F=='/') - root_dir = self.client.get('/rest/directories/files?project={}&path=%2F&include_parent'.format(project)) - root_id = root_dir.data['id'] + root_dir = self.client.get( + "/rest/directories/files?project={}&path=%2F&include_parent".format(project) + ) + root_id = root_dir.data["id"] # getting dataset files from / - response = self.client.get('/rest/directories/files?cr_identifier={}&project={}&path=%2F&{}' - .format(cr_id, project, fields)) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, 'Directory must be empty') + response = self.client.get( + "/rest/directories/files?cr_identifier={}&project={}&path=%2F&{}".format( + cr_id, project, fields + ) + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, "Directory must be empty") # adding file_1 to dataset - cr['research_dataset']['files'] = [ + cr["research_dataset"]["files"] = [ { "identifier": file1_id, "title": file1_title, "use_category": { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/configuration" - } + }, } ] - cr['research_dataset']["preferred_identifier"] = cr_rd_identifier - response = self.client.put('/rest/datasets/{}'.format(cr_id), cr, format='json') + cr["research_dataset"]["preferred_identifier"] = cr_rd_identifier + response = self.client.put("/rest/datasets/{}".format(cr_id), cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # getting dataset files from / - response = self.client.get('/rest/directories/{}/files?cr_identifier={}&fields'.format(root_id, cr_id)) + response = self.client.get( + "/rest/directories/{}/files?cr_identifier={}&fields".format(root_id, cr_id) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - dirs = response.data['directories'] - self.assertEqual(len(dirs), 1, 'Expected 1 directory') - self.assertEqual(dirs[0]['file_count'], 1, 'Expected 1 file in directory %s' % dirs[0]['directory_path']) + dirs = response.data["directories"] + self.assertEqual(len(dirs), 1, "Expected 1 directory") + self.assertEqual( + dirs[0]["file_count"], + 1, + "Expected 1 file in directory %s" % dirs[0]["directory_path"], + ) # getting dataset files from /dir/ - response = self.client.get('/rest/directories/{}/files?cr_identifier={}&include_parent&{}' - .format(dirs[0]['id'], cr_id, fields)) - self.assertEqual(len(response.data['files']), 1, 'Expected 1 file in directory {}' - .format(dirs[0]['directory_path'])) - self.assertEqual(response.data['file_count'], len(response.data['files']), - 'Expected 1 file in parent file_count') + response = self.client.get( + "/rest/directories/{}/files?cr_identifier={}&include_parent&{}".format( + dirs[0]["id"], cr_id, fields + ) + ) + self.assertEqual( + len(response.data["files"]), + 1, + "Expected 1 file in directory {}".format(dirs[0]["directory_path"]), + ) + self.assertEqual( + response.data["file_count"], + len(response.data["files"]), + "Expected 1 file in parent file_count", + ) # getting non-dataset files from /dir/ - response = self.client.get('/rest/directories/{}/files?not_cr_identifier={}&include_parent&{}' - .format(dirs[0]['id'], cr_id, fields)) - self.assertEqual(len(response.data['files']), 2, 'Expected 2 files in directory {}' - .format(dirs[0]['directory_path'])) - self.assertEqual(response.data['file_count'], len(response.data['files']), - 'Expected 2 file in parent file_count') + response = self.client.get( + "/rest/directories/{}/files?not_cr_identifier={}&include_parent&{}".format( + dirs[0]["id"], cr_id, fields + ) + ) + self.assertEqual( + len(response.data["files"]), + 2, + "Expected 2 files in directory {}".format(dirs[0]["directory_path"]), + ) + self.assertEqual( + response.data["file_count"], + len(response.data["files"]), + "Expected 2 file in parent file_count", + ) # adding file2 and file3 to dataset - cr['research_dataset']['files'] = cr['research_dataset']['files'] + [ + cr["research_dataset"]["files"] = cr["research_dataset"]["files"] + [ { "identifier": file2_id, "title": file2_title, "use_category": { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/configuration" - } + }, }, { "identifier": file3_id, "title": file3_title, "use_category": { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/configuration" - } - } + }, + }, ] - cr['research_dataset']['preferred_identifier'] = cr_rd_identifier + cr["research_dataset"]["preferred_identifier"] = cr_rd_identifier - response = self.client.patch('/rest/datasets/{}'.format(cr_id), cr, format='json') + response = self.client.patch("/rest/datasets/{}".format(cr_id), cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # getting dataset files from /dir/ - response = self.client.get('/rest/directories/{}/files?cr_identifier={}&include_parent&{}' - .format(dirs[0]['id'], cr_id, fields)) - self.assertEqual(len(response.data['files']), 3, 'Expected 3 files in directory {}' - .format(dirs[0]['directory_path'])) - self.assertEqual(response.data['file_count'], len(response.data['files']), - 'Expected 3 file in parent file_count') + response = self.client.get( + "/rest/directories/{}/files?cr_identifier={}&include_parent&{}".format( + dirs[0]["id"], cr_id, fields + ) + ) + self.assertEqual( + len(response.data["files"]), + 3, + "Expected 3 files in directory {}".format(dirs[0]["directory_path"]), + ) + self.assertEqual( + response.data["file_count"], + len(response.data["files"]), + "Expected 3 file in parent file_count", + ) # getting non-dataset files from /dir/ - response = self.client.get('/rest/directories/{}/files?not_cr_identifier={}&include_parent&{}' - .format(dirs[0]['id'], cr_id, fields)) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, 'Directory must be empty') + response = self.client.get( + "/rest/directories/{}/files?not_cr_identifier={}&include_parent&{}".format( + dirs[0]["id"], cr_id, fields + ) + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, "Directory must be empty") # getting dataset files from / - response = self.client.get('/rest/directories/{}/files?cr_identifier={}&fields'.format(root_id, cr_id)) + response = self.client.get( + "/rest/directories/{}/files?cr_identifier={}&fields".format(root_id, cr_id) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - dirs = response.data['directories'] - self.assertEqual(len(dirs), 1, 'Expected 1 directory') - self.assertEqual(dirs[0]['file_count'], 3, 'Expected 3 files in directory %s' % dirs[0]['directory_path']) + dirs = response.data["directories"] + self.assertEqual(len(dirs), 1, "Expected 1 directory") + self.assertEqual( + dirs[0]["file_count"], + 3, + "Expected 3 files in directory %s" % dirs[0]["directory_path"], + ) # getting dataset files from / - response = self.client.get('/rest/directories/{}/files?not_cr_identifier={}&fields'.format(root_id, cr_id)) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, 'Directory must be empty') + response = self.client.get( + "/rest/directories/{}/files?not_cr_identifier={}&fields".format(root_id, cr_id) + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, "Directory must be empty") diff --git a/src/metax_api/tests/api/rest/base/views/files/read.py b/src/metax_api/tests/api/rest/base/views/files/read.py index 1e00dc6f..011ede0a 100755 --- a/src/metax_api/tests/api/rest/base/views/files/read.py +++ b/src/metax_api/tests/api/rest/base/views/files/read.py @@ -21,80 +21,94 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(FileApiReadCommon, cls).setUpClass() def setUp(self): - file_from_test_data = self._get_object_from_test_data('file') - self.identifier = file_from_test_data['identifier'] - self.pk = file_from_test_data['id'] + file_from_test_data = self._get_object_from_test_data("file") + self.identifier = file_from_test_data["identifier"] + self.pk = file_from_test_data["id"] self._use_http_authorization() class FileApiReadBasicTests(FileApiReadCommon): - def test_read_file_list(self): - response = self.client.get('/rest/files') + response = self.client.get("/rest/files") self.assertEqual(response.status_code, status.HTTP_200_OK) def test_read_file_list_filter_by_project(self): proj = File.objects.get(pk=1).project_identifier file_count = File.objects.filter(project_identifier=proj).count() - response = self.client.get('/rest/files?project_identifier=%s' % proj) + response = self.client.get("/rest/files?project_identifier=%s" % proj) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], file_count) + self.assertEqual(response.data["count"], file_count) def test_read_file_list_filter_by_project_and_path(self): proj = File.objects.get(pk=1).project_identifier path = "/project_x_FROZEN/Experiment_X/Phase_1/2017/01" file_count = File.objects.filter(project_identifier=proj, file_path__contains=path).count() - response = self.client.get('/rest/files?project_identifier=%s&file_path=%s' % (proj, path)) + response = self.client.get("/rest/files?project_identifier=%s&file_path=%s" % (proj, path)) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], file_count) + self.assertEqual(response.data["count"], file_count) # missing project_identifier - response = self.client.get('/rest/files?file_path=%s' % path) + response = self.client.get("/rest/files?file_path=%s" % path) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_read_file_details_by_pk(self): - response = self.client.get('/rest/files/%s' % self.pk) + response = self.client.get("/rest/files/%s" % self.pk) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(hasattr(response, 'data'), True, 'Request response object is missing attribute \'data\'') - self.assertEqual('file_name' in response.data, True) - self.assertEqual(response.data['identifier'], self.identifier) - self.assertEqual('identifier' in response.data['file_storage'], True) + self.assertEqual( + hasattr(response, "data"), + True, + "Request response object is missing attribute 'data'", + ) + self.assertEqual("file_name" in response.data, True) + self.assertEqual(response.data["identifier"], self.identifier) + self.assertEqual("identifier" in response.data["file_storage"], True) def test_read_file_details_by_identifier(self): - response = self.client.get('/rest/files/%s' % self.identifier) + response = self.client.get("/rest/files/%s" % self.identifier) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(hasattr(response, 'data'), True, 'Request response object is missing attribute \'data\'') - self.assertEqual('file_name' in response.data.keys(), True) - self.assertEqual(response.data['identifier'], self.identifier) + self.assertEqual( + hasattr(response, "data"), + True, + "Request response object is missing attribute 'data'", + ) + self.assertEqual("file_name" in response.data.keys(), True) + self.assertEqual(response.data["identifier"], self.identifier) def test_read_file_details_not_found(self): - response = self.client.get('/rest/files/shouldnotexist') + response = self.client.get("/rest/files/shouldnotexist") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_read_file_details_checksum_relation(self): - response = self.client.get('/rest/files/%s' % self.pk) + response = self.client.get("/rest/files/%s" % self.pk) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('checksum' in response.data, True) - self.assertEqual('value' in response.data['checksum'], True) + self.assertEqual("checksum" in response.data, True) + self.assertEqual("value" in response.data["checksum"], True) def test_expand_relations(self): - response = self.client.get('/rest/files/1?expand_relation=file_storage,parent_directory') + response = self.client.get("/rest/files/1?expand_relation=file_storage,parent_directory") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('file_storage_json' in response.data['file_storage'], True, response.data['file_storage']) - self.assertEqual('date_created' in response.data['parent_directory'], True, response.data['parent_directory']) + self.assertEqual( + "file_storage_json" in response.data["file_storage"], + True, + response.data["file_storage"], + ) + self.assertEqual( + "date_created" in response.data["parent_directory"], + True, + response.data["parent_directory"], + ) class FileApiReadGetRelatedDatasets(FileApiReadCommon): - def test_get_related_datasets_ok_1(self): """ File pk 1 should belong to only 3 datasets """ - response = self.client.post('/rest/files/datasets', [1], format='json') + response = self.client.post("/rest/files/datasets", [1], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 3) @@ -102,8 +116,10 @@ def test_get_related_datasets_ok_2(self): """ File identifiers listed below should belong to 5 datasets """ - file_identifiers = File.objects.filter(id__in=[1, 2, 3, 4, 5]).values_list('identifier', flat=True) - response = self.client.post('/rest/files/datasets', file_identifiers, format='json') + file_identifiers = File.objects.filter(id__in=[1, 2, 3, 4, 5]).values_list( + "identifier", flat=True + ) + response = self.client.post("/rest/files/datasets", file_identifiers, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) @@ -111,31 +127,37 @@ def test_keysonly(self): """ Parameter ?keysonly should return just values """ - response = self.client.post('/rest/files/datasets?keys=files&keysonly', [1, 2, 121], format='json') + response = self.client.post( + "/rest/files/datasets?keys=files&keysonly", [1, 2, 121], format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 2) # pid:urn:121 does not belong to any dataset - self.assertEqual(type(response.data), list, type(response.data)) # no dict keys + self._assert_results_length(response, 2) # pid:urn:121 does not belong to any dataset + self.assertEqual(type(response.data), list, type(response.data)) # no dict keys - response = self.client.post('/rest/files/datasets?keys=files&keysonly=false', [1, 2], format='json') + response = self.client.post( + "/rest/files/datasets?keys=files&keysonly=false", [1, 2], format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(type(response.data), dict, response.data) # Return by keys + self.assertEqual(type(response.data), dict, response.data) # Return by keys - response = self.client.post('/rest/files/datasets?keys=datasets&keysonly', [1, 2, 14], format='json') + response = self.client.post( + "/rest/files/datasets?keys=datasets&keysonly", [1, 2, 14], format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 2) # Only datasets 1 and 2 have files - self.assertEqual(type(response.data), list, type(response.data)) # no dict keys + self._assert_results_length(response, 2) # Only datasets 1 and 2 have files + self.assertEqual(type(response.data), list, type(response.data)) # no dict keys def test_get_detailed_related_datasets_ok_1(self): """ File identifiers listed below should belong to 3 datasets """ - response = self.client.post('/rest/files/datasets?keys=files', [1], format='json') + response = self.client.post("/rest/files/datasets?keys=files", [1], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 1) self.assertEqual(len(list(response.data.values())[0]), 3, response.data) # Support for ?detailed - response = self.client.post('/rest/files/datasets?detailed', [1], format='json') + response = self.client.post("/rest/files/datasets?detailed", [1], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 1) self.assertEqual(len(list(response.data.values())[0]), 3, response.data) @@ -146,7 +168,9 @@ def test_get_detailed_related_datasets_ok_2(self): """ file_identifiers = [1, 2, 3, 4, 5] - response = self.client.post('/rest/files/datasets?keys=files', file_identifiers, format='json') + response = self.client.post( + "/rest/files/datasets?keys=files", file_identifiers, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) @@ -154,9 +178,17 @@ def test_get_detailed_related_datasets_ok_2(self): self.assertEqual(len(set(sum(response.data.values(), []))), 5, response.data) # check if identifiers work - file_identifiers = ['pid:urn:1', 'pid:urn:2', 'pid:urn:3', 'pid:urn:4', 'pid:urn:5'] - - response = self.client.post('/rest/files/datasets?keys=files', file_identifiers, format='json') + file_identifiers = [ + "pid:urn:1", + "pid:urn:2", + "pid:urn:3", + "pid:urn:4", + "pid:urn:5", + ] + + response = self.client.post( + "/rest/files/datasets?keys=files", file_identifiers, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) @@ -167,7 +199,7 @@ def test_get_detailed_related_files_ok_1(self): """ Dataset identifiers listed below should have 2 files """ - response = self.client.post('/rest/files/datasets?keys=datasets', [1], format='json') + response = self.client.post("/rest/files/datasets?keys=datasets", [1], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 1) self.assertEqual(len(list(response.data.values())[0]), 2, response.data) @@ -178,7 +210,9 @@ def test_get_detailed_related_files_ok_2(self): """ dataset_identifiers = [1, 2, 3, 4, 5] - response = self.client.post('/rest/files/datasets?keys=datasets', dataset_identifiers, format='json') + response = self.client.post( + "/rest/files/datasets?keys=datasets", dataset_identifiers, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) @@ -186,13 +220,17 @@ def test_get_detailed_related_files_ok_2(self): self.assertEqual(len(set(sum(response.data.values(), []))), 10, response.data) # check if identifiers work - dataset_identifiers = ["cr955e904-e3dd-4d7e-99f1-3fed446f96d1", - "cr955e904-e3dd-4d7e-99f1-3fed446f96d2", - "cr955e904-e3dd-4d7e-99f1-3fed446f96d3", - "cr955e904-e3dd-4d7e-99f1-3fed446f96d4", - "cr955e904-e3dd-4d7e-99f1-3fed446f96d5"] - - response = self.client.post('/rest/files/datasets?keys=datasets', dataset_identifiers, format='json') + dataset_identifiers = [ + "cr955e904-e3dd-4d7e-99f1-3fed446f96d1", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d2", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d3", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d4", + "cr955e904-e3dd-4d7e-99f1-3fed446f96d5", + ] + + response = self.client.post( + "/rest/files/datasets?keys=datasets", dataset_identifiers, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 5) @@ -203,61 +241,82 @@ def test_get_right_files_and_datasets(self): """ Check that returned files and datasets are the right ones """ - testfile = self._get_object_from_test_data('file') + testfile = self._get_object_from_test_data("file") - cr = self.client.get('/rest/datasets/10', format='json') + cr = self.client.get("/rest/datasets/10", format="json") self.assertEqual(cr.status_code, status.HTTP_200_OK, cr.data) - response = self.client.post('/rest/files/datasets?keys=datasets', [cr.data['identifier']], format='json') + response = self.client.post( + "/rest/files/datasets?keys=datasets", [cr.data["identifier"]], format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # cr 10 has 2 default files for keys, values in response.data.items(): - self.assertEqual(keys == 'cr955e904-e3dd-4d7e-99f1-3fed446f9610', True, response.data) - self.assertEqual('pid:urn:19' and 'pid:urn:20' in values, True, response.data) + self.assertEqual(keys == "cr955e904-e3dd-4d7e-99f1-3fed446f9610", True, response.data) + self.assertEqual("pid:urn:19" and "pid:urn:20" in values, True, response.data) - response = self.client.post('/rest/files/datasets?keys=files', [testfile['identifier']], format='json') + response = self.client.post( + "/rest/files/datasets?keys=files", [testfile["identifier"]], format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # file 1 belongs to 3 datasets for keys, values in response.data.items(): - self.assertEqual(keys == 'pid:urn:1', True, response.data) - self.assertEqual('cr955e904-e3dd-4d7e-99f1-3fed446f96d1' and 'cr955e904-e3dd-4d7e-99f1-3fed446f9612' - and 'cr955e904-e3dd-4d7e-99f1-3fed446f9611' in values, True, response.data) + self.assertEqual(keys == "pid:urn:1", True, response.data) + self.assertEqual( + "cr955e904-e3dd-4d7e-99f1-3fed446f96d1" + and "cr955e904-e3dd-4d7e-99f1-3fed446f9612" + and "cr955e904-e3dd-4d7e-99f1-3fed446f9611" in values, + True, + response.data, + ) # Dataset 11 has 20 files in a directory - cr = self.client.get('/rest/datasets/11', format='json') + cr = self.client.get("/rest/datasets/11", format="json") self.assertEqual(cr.status_code, status.HTTP_200_OK, cr.data) # Compare using return from different api - files_in_cr11 = self.client.get('/rest/datasets/11/files', format='json') + files_in_cr11 = self.client.get("/rest/datasets/11/files", format="json") self.assertEqual(files_in_cr11.status_code, status.HTTP_200_OK, files_in_cr11.data) identifiers = [] - [identifiers.append(i['identifier']) for i in files_in_cr11.data] + [identifiers.append(i["identifier"]) for i in files_in_cr11.data] - response = self.client.post('/rest/files/datasets?keys=datasets', [11], format='json') + response = self.client.post("/rest/files/datasets?keys=datasets", [11], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # This should have the same file id's as the return from /rest/v2/datasets/11/files - self.assertEqual(sorted(response.data['cr955e904-e3dd-4d7e-99f1-3fed446f9611']), sorted(identifiers), - response.data) - - response = self.client.post('/rest/files/datasets?keys=files', ['pid:urn:20'], format='json') + self.assertEqual( + sorted(response.data["cr955e904-e3dd-4d7e-99f1-3fed446f9611"]), + sorted(identifiers), + response.data, + ) + + response = self.client.post( + "/rest/files/datasets?keys=files", ["pid:urn:20"], format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # Dataset 11 should be found from results - self.assertTrue('cr955e904-e3dd-4d7e-99f1-3fed446f9611' in response.data['pid:urn:20'], response.data) + self.assertTrue( + "cr955e904-e3dd-4d7e-99f1-3fed446f9611" in response.data["pid:urn:20"], + response.data, + ) def test_get_related_datasets_files_not_found(self): """ When the files themselves are not found, 404 should be returned """ - response = self.client.post('/rest/files/datasets', ['doesnotexist'], format='json') + response = self.client.post("/rest/files/datasets", ["doesnotexist"], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 0) - response = self.client.post('/rest/files/datasets?keys=files', ['doesnotexist'], format='json') + response = self.client.post( + "/rest/files/datasets?keys=files", ["doesnotexist"], format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 0) # Support for ?detailed - response = self.client.post('/rest/files/datasets?detailed', ['doesnotexist'], format='json') + response = self.client.post( + "/rest/files/datasets?detailed", ["doesnotexist"], format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 0) @@ -267,27 +326,30 @@ def test_get_related_datasets_records_not_found(self): """ with connection.cursor() as cr: # detach file pk 1 from any datasets - cr.execute('delete from metax_api_catalogrecord_files where file_id = 1') + cr.execute("delete from metax_api_catalogrecord_files where file_id = 1") - response = self.client.post('/rest/files/datasets', [1], format='json') + response = self.client.post("/rest/files/datasets", [1], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 0) - response = self.client.post('/rest/files/datasets?keys=files', [1], format='json') + response = self.client.post("/rest/files/datasets?keys=files", [1], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 0) # Support for ?detailed - response = self.client.post('/rest/files/datasets?detailed', [1], format='json') + response = self.client.post("/rest/files/datasets?detailed", [1], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self._assert_results_length(response, 0) def _assert_results_length(self, response, length): - self.assertTrue(isinstance(response.data, dict) or isinstance(response.data, list), response.data) + self.assertTrue( + isinstance(response.data, dict) or isinstance(response.data, list), + response.data, + ) self.assertEqual(len(response.data), length) -class FileApiReadEndUserAccess(FileApiReadCommon): +class FileApiReadEndUserAccess(FileApiReadCommon): def setUp(self): super().setUp() self.token = get_test_oidc_token() @@ -295,32 +357,36 @@ def setUp(self): @responses.activate def test_user_can_read_owned_files(self): - ''' + """ Ensure users can only read files owned by them from /rest/files api. - ''' + """ # first read files without project access - should fail - self._use_http_authorization(method='bearer', token=self.token) + self._use_http_authorization(method="bearer", token=self.token) proj = File.objects.get(pk=1).project_identifier - response = self.client.get('/rest/files/1') + response = self.client.get("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - response = self.client.get('/rest/files?project_identifier=%s' % proj) + response = self.client.get("/rest/files?project_identifier=%s" % proj) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - response = self.client.get('/rest/files?no_pagination') + response = self.client.get("/rest/files?no_pagination") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data), 0, 'should return 200 OK, but user projects has no files') + self.assertEqual( + len(response.data), + 0, + "should return 200 OK, but user projects has no files", + ) # set user to same project as previous files and try again. should now succeed - self.token['group_names'].append('IDA01:%s' % proj) - self._use_http_authorization(method='bearer', token=self.token) + self.token["group_names"].append("IDA01:%s" % proj) + self._use_http_authorization(method="bearer", token=self.token) - response = self.client.get('/rest/files') + response = self.client.get("/rest/files") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data) > 0, True, 'user should only see their own files') + self.assertEqual(len(response.data) > 0, True, "user should only see their own files") - response = self.client.get('/rest/files/1') + response = self.client.get("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.get('/rest/files?project_identifier=%s' % proj) + response = self.client.get("/rest/files?project_identifier=%s" % proj) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/metax_api/tests/api/rest/base/views/files/write.py b/src/metax_api/tests/api/rest/base/views/files/write.py index 211bac5c..b93816eb 100755 --- a/src/metax_api/tests/api/rest/base/views/files/write.py +++ b/src/metax_api/tests/api/rest/base/views/files/write.py @@ -25,18 +25,18 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(FileApiWriteCommon, cls).setUpClass() def setUp(self): """ Reloaded for every test case """ - call_command('loaddata', test_data_file_path, verbosity=0) - file_from_test_data = self._get_object_from_test_data('file') - self.identifier = file_from_test_data['identifier'] - self.pidentifier = file_from_test_data['project_identifier'] - self.file_name = file_from_test_data['file_name'] + call_command("loaddata", test_data_file_path, verbosity=0) + file_from_test_data = self._get_object_from_test_data("file") + self.identifier = file_from_test_data["identifier"] + self.pidentifier = file_from_test_data["project_identifier"] + self.file_name = file_from_test_data["file_name"] """ New data that is sent to the server for POST, PUT, PATCH requests. Modified @@ -47,21 +47,25 @@ def setUp(self): self._use_http_authorization() def _get_new_test_data(self): - from_test_data = self._get_object_from_test_data('file', requested_index=0) - from_test_data.update({ - "checksum": { - "value": "habeebit", - "algorithm": "SHA-256", - "checked": "2017-05-23T10:07:22.559656Z", - }, - "file_name": "file_name_1", - "file_path": from_test_data['file_path'].replace('/some/path', '/some/other_path'), - "identifier": "urn:nbn:fi:csc-ida201401200000000001", - "file_storage": self._get_object_from_test_data('filestorage', requested_index=0) - }) - from_test_data['file_path'] = from_test_data['file_path'].replace('/Experiment_X/', '/test/path/') - from_test_data['project_identifier'] = 'test_project_identifier' - del from_test_data['id'] + from_test_data = self._get_object_from_test_data("file", requested_index=0) + from_test_data.update( + { + "checksum": { + "value": "habeebit", + "algorithm": "SHA-256", + "checked": "2017-05-23T10:07:22.559656Z", + }, + "file_name": "file_name_1", + "file_path": from_test_data["file_path"].replace("/some/path", "/some/other_path"), + "identifier": "urn:nbn:fi:csc-ida201401200000000001", + "file_storage": self._get_object_from_test_data("filestorage", requested_index=0), + } + ) + from_test_data["file_path"] = from_test_data["file_path"].replace( + "/Experiment_X/", "/test/path/" + ) + from_test_data["project_identifier"] = "test_project_identifier" + del from_test_data["id"] return from_test_data def _get_second_new_test_data(self): @@ -73,7 +77,7 @@ def _get_second_new_test_data(self): def _count_dirs_from_path(self, file_path): expected_dirs_count = 1 dir_name = dirname(file_path) - while dir_name != '/': + while dir_name != "/": dir_name = dirname(dir_name) expected_dirs_count += 1 return expected_dirs_count @@ -83,34 +87,34 @@ def _check_project_root_byte_size_and_file_count(self, project_identifier): A rather simple test to fetch the root directory of a project, and verify that the root's calculated total byte size and file count match what exists in the db. """ - byte_size = File.objects.filter(project_identifier=project_identifier) \ - .aggregate(Sum('byte_size'))['byte_size__sum'] + byte_size = File.objects.filter(project_identifier=project_identifier).aggregate( + Sum("byte_size") + )["byte_size__sum"] file_count = File.objects.filter(project_identifier=project_identifier).count() - response = self.client.get('/rest/directories/root?project=%s' % project_identifier) + response = self.client.get("/rest/directories/root?project=%s" % project_identifier) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['byte_size'], byte_size) - self.assertEqual(response.data['file_count'], file_count) + self.assertEqual(response.data["byte_size"], byte_size) + self.assertEqual(response.data["file_count"], file_count) def _change_file_path(self, file, new_name): - file['file_path'] = file['file_path'].replace(file['file_name'], new_name) - file['file_name'] = new_name + file["file_path"] = file["file_path"].replace(file["file_name"], new_name) + file["file_name"] = new_name class FileApiWriteReferenceDataValidationTests(FileApiWriteCommon): - @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('updatereferencedata', verbosity=0) + call_command("updatereferencedata", verbosity=0) super(FileApiWriteReferenceDataValidationTests, cls).setUpClass() def setUp(self): super().setUp() cache = RedisClient() - ffv_refdata = cache.get('reference_data')['reference_data']['file_format_version'] + ffv_refdata = cache.get("reference_data")["reference_data"]["file_format_version"] # File format version entry in reference data that has some output_format_version self.ff_with_version = None @@ -122,73 +126,116 @@ def setUp(self): for ffv_obj in ffv_refdata: if self.ff_with_different_version is None and self.ff_with_version is not None: - if ffv_obj['input_file_format'] == self.ff_with_version['input_file_format']: + if ffv_obj["input_file_format"] == self.ff_with_version["input_file_format"]: self.ff_with_different_version = ffv_obj - if self.ff_with_version is None and ffv_obj['output_format_version']: + if self.ff_with_version is None and ffv_obj["output_format_version"]: self.ff_with_version = ffv_obj - if self.ff_without_version is None and not ffv_obj['output_format_version']: + if self.ff_without_version is None and not ffv_obj["output_format_version"]: self.ff_without_version = ffv_obj - self.assertTrue(self.ff_with_version['output_format_version'] != '') - self.assertTrue(self.ff_with_different_version['output_format_version'] != '') - self.assertTrue(self.ff_with_version['input_file_format'] == - self.ff_with_different_version['input_file_format']) - self.assertTrue(self.ff_with_version['output_format_version'] != - self.ff_with_different_version['output_format_version']) - self.assertTrue(self.ff_without_version['output_format_version'] == '') - - def test_file_format_version_with_invalid_file_format_when_format_version_given_1(self): - self.test_new_data['file_characteristics']['format_version'] = 'any' - response = self.client.post('/rest/files', self.test_new_data, format="json") + self.assertTrue(self.ff_with_version["output_format_version"] != "") + self.assertTrue(self.ff_with_different_version["output_format_version"] != "") + self.assertTrue( + self.ff_with_version["input_file_format"] + == self.ff_with_different_version["input_file_format"] + ) + self.assertTrue( + self.ff_with_version["output_format_version"] + != self.ff_with_different_version["output_format_version"] + ) + self.assertTrue(self.ff_without_version["output_format_version"] == "") + + def test_file_format_version_with_invalid_file_format_when_format_version_given_1( + self, + ): + self.test_new_data["file_characteristics"]["format_version"] = "any" + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_characteristics' in response.data.keys(), True) - self.assertEqual('file_characteristics.file_format' in response.data['file_characteristics'], True) - - def test_file_format_version_with_invalid_file_format_when_format_version_given_2(self): - self.test_new_data['file_characteristics']['file_format'] = 'nonexisting' - self.test_new_data['file_characteristics']['format_version'] = 'any' - response = self.client.post('/rest/files', self.test_new_data, format="json") + self.assertEqual("file_characteristics" in response.data.keys(), True) + self.assertEqual( + "file_characteristics.file_format" in response.data["file_characteristics"], + True, + ) + + def test_file_format_version_with_invalid_file_format_when_format_version_given_2( + self, + ): + self.test_new_data["file_characteristics"]["file_format"] = "nonexisting" + self.test_new_data["file_characteristics"]["format_version"] = "any" + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_characteristics' in response.data.keys(), True) - self.assertEqual('file_characteristics.file_format' in response.data['file_characteristics'], True) - - def test_file_format_version_with_invalid_format_version_when_file_format_has_versions_1(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_with_version['input_file_format'] - response = self.client.post('/rest/files', self.test_new_data, format="json") + self.assertEqual("file_characteristics" in response.data.keys(), True) + self.assertEqual( + "file_characteristics.file_format" in response.data["file_characteristics"], + True, + ) + + def test_file_format_version_with_invalid_format_version_when_file_format_has_versions_1( + self, + ): + self.test_new_data["file_characteristics"]["file_format"] = self.ff_with_version[ + "input_file_format" + ] + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_characteristics' in response.data.keys(), True) - self.assertEqual('file_characteristics.format_version' in response.data['file_characteristics'], True) - - def test_file_format_version_with_invalid_format_version_when_file_format_has_versions_2(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_with_version['input_file_format'] - self.test_new_data['file_characteristics']['format_version'] = 'nonexisting' - response = self.client.post('/rest/files', self.test_new_data, format="json") + self.assertEqual("file_characteristics" in response.data.keys(), True) + self.assertEqual( + "file_characteristics.format_version" in response.data["file_characteristics"], + True, + ) + + def test_file_format_version_with_invalid_format_version_when_file_format_has_versions_2( + self, + ): + self.test_new_data["file_characteristics"]["file_format"] = self.ff_with_version[ + "input_file_format" + ] + self.test_new_data["file_characteristics"]["format_version"] = "nonexisting" + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_characteristics' in response.data.keys(), True) - self.assertEqual('file_characteristics.format_version' in response.data['file_characteristics'], True) - - def test_file_format_version_with_empty_format_version_when_file_format_has_no_version_1(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_without_version['input_file_format'] - response = self.client.post('/rest/files', self.test_new_data, format="json") + self.assertEqual("file_characteristics" in response.data.keys(), True) + self.assertEqual( + "file_characteristics.format_version" in response.data["file_characteristics"], + True, + ) + + def test_file_format_version_with_empty_format_version_when_file_format_has_no_version_1( + self, + ): + self.test_new_data["file_characteristics"]["file_format"] = self.ff_without_version[ + "input_file_format" + ] + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - def test_file_format_version_with_empty_format_version_when_file_format_has_no_version_2(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_without_version['input_file_format'] - self.test_new_data['file_characteristics']['format_version'] = '' - response = self.client.post('/rest/files', self.test_new_data, format="json") + def test_file_format_version_with_empty_format_version_when_file_format_has_no_version_2( + self, + ): + self.test_new_data["file_characteristics"]["file_format"] = self.ff_without_version[ + "input_file_format" + ] + self.test_new_data["file_characteristics"]["format_version"] = "" + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def test_file_format_version_with_valid_file_format_and_valid_file_version_1(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_with_version['input_file_format'] - self.test_new_data['file_characteristics']['format_version'] = self.ff_with_version['output_format_version'] - response = self.client.post('/rest/files', self.test_new_data, format="json") + self.test_new_data["file_characteristics"]["file_format"] = self.ff_with_version[ + "input_file_format" + ] + self.test_new_data["file_characteristics"]["format_version"] = self.ff_with_version[ + "output_format_version" + ] + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_file_format_version_with_valid_file_format_and_valid_file_version_2(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_with_version['input_file_format'] - self.test_new_data['file_characteristics']['format_version'] = \ - self.ff_with_different_version['output_format_version'] - response = self.client.post('/rest/files', self.test_new_data, format="json") + self.test_new_data["file_characteristics"]["file_format"] = self.ff_with_version[ + "input_file_format" + ] + self.test_new_data["file_characteristics"][ + "format_version" + ] = self.ff_with_different_version["output_format_version"] + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) # update tests @@ -197,11 +244,15 @@ def test_file_characteristics_is_validated_on_update(self): """ Ensure validation also works when updating existing files. """ - self.test_new_data['file_characteristics']['file_format'] = self.ff_without_version['input_file_format'] - response = self.client.post('/rest/files', self.test_new_data, format="json") + self.test_new_data["file_characteristics"]["file_format"] = self.ff_without_version[ + "input_file_format" + ] + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - response = self.client.put('/rest/files/%s' % response.data['identifier'], response.data, format="json") + response = self.client.put( + "/rest/files/%s" % response.data["identifier"], response.data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) @@ -216,81 +267,92 @@ class FileApiWriteCreateTests(FileApiWriteCommon): def test_create_file(self): # note: leading and trailing whitespace must be preserved. - newly_created_file_name = " MX .201015_Suomessa_tavattavat_ruokasammakot_ovat_väritykseltään_vaihtelevia_" \ - "osa_on_ruskeita,_osa_kirkkaankin_vihreitä._Vihersammakoiden_silmät_ovat_kohtalaisen_korkealla_päälae" \ + newly_created_file_name = ( + " MX .201015_Suomessa_tavattavat_ruokasammakot_ovat_väritykseltään_vaihtelevia_" + "osa_on_ruskeita,_osa_kirkkaankin_vihreitä._Vihersammakoiden_silmät_ovat_kohtalaisen_korkealla_päälae" "lla._Sammakkolampi.fi_CC-BY-NC-4.0_thumb.jpg.meta " - self.test_new_data['file_name'] = newly_created_file_name - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' + ) + self.test_new_data["file_name"] = newly_created_file_name + self.test_new_data["identifier"] = "urn:nbn:fi:csc-thisisanewurn" - response = self.client.post('/rest/files', self.test_new_data, format="json") + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('file_name' in response.data.keys(), True) - self.assertEqual(response.data['file_name'], newly_created_file_name) - self._check_project_root_byte_size_and_file_count(response.data['project_identifier']) + self.assertEqual("file_name" in response.data.keys(), True) + self.assertEqual(response.data["file_name"], newly_created_file_name) + self._check_project_root_byte_size_and_file_count(response.data["project_identifier"]) def test_create_file_error_identifier_exists(self): # first ok - response = self.client.post('/rest/files', self.test_new_data, format="json") + response = self.client.post("/rest/files", self.test_new_data, format="json") # second should give error - response = self.client.post('/rest/files', self.test_new_data, format="json") + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('identifier' in response.data.keys(), True) - self.assertEqual('already exists' in response.data['identifier'][0], True) + self.assertEqual("identifier" in response.data.keys(), True) + self.assertEqual("already exists" in response.data["identifier"][0], True) def test_allow_creating_previously_deleted_file(self): """ It should be possible to delete a file, and then create the exact same file again without letting the removed file conflict. """ - response = self.client.post('/rest/files', self.test_new_data, format="json") - response = self.client.delete('/rest/files/%d' % response.data['id'], format="json") + response = self.client.post("/rest/files", self.test_new_data, format="json") + response = self.client.delete("/rest/files/%d" % response.data["id"], format="json") - response = self.client.post('/rest/files', self.test_new_data, format="json") + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def test_create_file_error_json_validation(self): - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' - self.test_new_data['file_characteristics'] = { + self.test_new_data["identifier"] = "urn:nbn:fi:csc-thisisanewurn" + self.test_new_data["file_characteristics"] = { "application_name": "Application Name", "description": "A nice description 0000000010", "metadata_modified": 12345, "file_created": "2014-01-17T08:19:31Z", "encoding": "utf-8", - "title": "A title 0000000010" + "title": "A title 0000000010", } - response = self.client.post('/rest/files', self.test_new_data, format="json") + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_characteristics' in response.data.keys(), True, - 'The error should concern the field file_characteristics') - self.assertEqual('metadata_modified' in response.data['file_characteristics'][0], True, - 'The error should contain the name of the erroneous field') - self.assertEqual('Json path:' in response.data['file_characteristics'][0], True, - 'The error should contain the json path') + self.assertEqual( + "file_characteristics" in response.data.keys(), + True, + "The error should concern the field file_characteristics", + ) + self.assertEqual( + "metadata_modified" in response.data["file_characteristics"][0], + True, + "The error should contain the name of the erroneous field", + ) + self.assertEqual( + "Json path:" in response.data["file_characteristics"][0], + True, + "The error should contain the json path", + ) def test_create_file_allowed_checksum_algorithm(self): - self.test_new_data['checksum']['algorithm'] = 'SHA-512' + self.test_new_data["checksum"]["algorithm"] = "SHA-512" - response = self.client.post('/rest/files', self.test_new_data, format="json") + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['checksum']['algorithm'], 'SHA-512') + self.assertEqual(response.data["checksum"]["algorithm"], "SHA-512") - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-md5' - self.test_new_data['file_path'] = '/md5/filepath/md5-filename' - self.test_new_data['file_name'] = 'md5-filename' - self.test_new_data['checksum']['algorithm'] = 'MD5' + self.test_new_data["identifier"] = "urn:nbn:fi:csc-md5" + self.test_new_data["file_path"] = "/md5/filepath/md5-filename" + self.test_new_data["file_name"] = "md5-filename" + self.test_new_data["checksum"]["algorithm"] = "MD5" - response = self.client.post('/rest/files', self.test_new_data, format="json") + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['checksum']['algorithm'], 'MD5') + self.assertEqual(response.data["checksum"]["algorithm"], "MD5") def test_create_file_not_allowed_checksum_algorithm(self): from django.db import transaction - for algo in ['sha2', 'sha256', 'sha-256']: + for algo in ["sha2", "sha256", "sha-256"]: # run POST requests inside db transaction to ensure django testcase transactions # work correctly. https://stackoverflow.com/a/23326971/1201945 this probably has # somethind to do with the fact that POST requests to /rest/files do not normally @@ -300,45 +362,60 @@ def test_create_file_not_allowed_checksum_algorithm(self): # alternative for below would be to use optional query param ?dryrun=true, which # causes the request to be executed inside a transaction too. with transaction.atomic(): - self.test_new_data['checksum']['algorithm'] = algo - response = self.client.post('/rest/files', self.test_new_data, format="json") + self.test_new_data["checksum"]["algorithm"] = algo + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('checksum_algorithm' in response.data, True) + self.assertEqual("checksum_algorithm" in response.data, True) # # create list operations # def test_create_file_list(self): - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' - self._change_file_path(self.test_new_data, 'one_file.txt') + self.test_new_data["identifier"] = "urn:nbn:fi:csc-thisisanewurn" + self._change_file_path(self.test_new_data, "one_file.txt") - self.second_test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurnalso' - self._change_file_path(self.second_test_new_data, 'two_file.txt') + self.second_test_new_data["identifier"] = "urn:nbn:fi:csc-thisisanewurnalso" + self._change_file_path(self.second_test_new_data, "two_file.txt") - response = self.client.post('/rest/files', [self.test_new_data, self.second_test_new_data], format="json") + response = self.client.post( + "/rest/files", + [self.test_new_data, self.second_test_new_data], + format="json", + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('object' in response.data['success'][0].keys(), True) - self.assertEqual(len(response.data['failed']), 0, response.data['failed']) - self.assertEqual(len(response.data['success']), 2) - self._check_project_root_byte_size_and_file_count(response.data['success'][0]['object']['project_identifier']) + self.assertEqual("failed" in response.data.keys(), True) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual("object" in response.data["success"][0].keys(), True) + self.assertEqual(len(response.data["failed"]), 0, response.data["failed"]) + self.assertEqual(len(response.data["success"]), 2) + self._check_project_root_byte_size_and_file_count( + response.data["success"][0]["object"]["project_identifier"] + ) # ensure structure of some specific fields is the same as when single files are created - self.assertEqual('identifier' in response.data['success'][0]['object']['file_storage'], True) - self.assertEqual('identifier' in response.data['success'][0]['object']['parent_directory'], True) - self.assertEqual('checksum' in response.data['success'][0]['object'], True) - self.assertEqual('value' in response.data['success'][0]['object']['checksum'], True) + self.assertEqual( + "identifier" in response.data["success"][0]["object"]["file_storage"], True + ) + self.assertEqual( + "identifier" in response.data["success"][0]["object"]["parent_directory"], + True, + ) + self.assertEqual("checksum" in response.data["success"][0]["object"], True) + self.assertEqual("value" in response.data["success"][0]["object"]["checksum"], True) def test_create_file_list_error_one_fails(self): - newly_created_file_name = 'newly_created_file_name' - self.test_new_data['file_name'] = newly_created_file_name - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' + newly_created_file_name = "newly_created_file_name" + self.test_new_data["file_name"] = newly_created_file_name + self.test_new_data["identifier"] = "urn:nbn:fi:csc-thisisanewurn" # same as above - should fail - self.second_test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' + self.second_test_new_data["identifier"] = "urn:nbn:fi:csc-thisisanewurn" - response = self.client.post('/rest/files', [self.test_new_data, self.second_test_new_data], format="json") + response = self.client.post( + "/rest/files", + [self.test_new_data, self.second_test_new_data], + format="json", + ) """ List response looks like @@ -357,40 +434,50 @@ def test_create_file_list_error_one_fails(self): } """ self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('object' in response.data['failed'][0].keys(), True) - self.assertEqual('file_name' in response.data['failed'][0]['object'].keys(), True) - self.assertEqual('identifier' in response.data['failed'][0]['errors'], True, - 'The error should have been about an already existing identifier') + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual("failed" in response.data.keys(), True) + self.assertEqual("object" in response.data["failed"][0].keys(), True) + self.assertEqual("file_name" in response.data["failed"][0]["object"].keys(), True) + self.assertEqual( + "identifier" in response.data["failed"][0]["errors"], + True, + "The error should have been about an already existing identifier", + ) def test_parameter_ignore_already_exists_errors(self): - newly_created_file_name = 'newly_created_file_name' - self.test_new_data['file_name'] = newly_created_file_name - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' + newly_created_file_name = "newly_created_file_name" + self.test_new_data["file_name"] = newly_created_file_name + self.test_new_data["identifier"] = "urn:nbn:fi:csc-thisisanewurn" # same as above - should cause an error. - self.second_test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' + self.second_test_new_data["identifier"] = "urn:nbn:fi:csc-thisisanewurn" - response = self.client.post('/rest/files?ignore_already_exists_errors', - [self.test_new_data, self.second_test_new_data], format="json") + response = self.client.post( + "/rest/files?ignore_already_exists_errors", + [self.test_new_data, self.second_test_new_data], + format="json", + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assertEqual(len(response.data), 2) - self.assertEqual('already exists' in response.data['success'][1]['object']['detail'], True) + self.assertEqual("already exists" in response.data["success"][1]["object"]["detail"], True) def test_create_file_list_error_all_fail(self): - newly_created_file_name = 'newly_created_file_name' - self.test_new_data['file_name'] = newly_created_file_name + newly_created_file_name = "newly_created_file_name" + self.test_new_data["file_name"] = newly_created_file_name # identifier is a required field, should fail - self.test_new_data['identifier'] = None - self.second_test_new_data['identifier'] = None - - response = self.client.post('/rest/files', [self.test_new_data, self.second_test_new_data], format="json") + self.test_new_data["identifier"] = None + self.second_test_new_data["identifier"] = None + + response = self.client.post( + "/rest/files", + [self.test_new_data, self.second_test_new_data], + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('object' in response.data['failed'][0].keys(), True) - self.assertEqual(len(response.data['success']), 0) - self.assertEqual(len(response.data['failed']), 2) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual("failed" in response.data.keys(), True) + self.assertEqual("object" in response.data["failed"][0].keys(), True) + self.assertEqual(len(response.data["success"]), 0) + self.assertEqual(len(response.data["failed"]), 2) class FileApiWriteCreateDirectoriesTests(FileApiWriteCommon): @@ -406,19 +493,21 @@ def test_create_file_hierarchy_from_single_file(self): """ f = self._form_complex_list_from_test_file()[0] - file_path = '/project_y_FROZEN/Experiment_1/path/of/lonely/file_and_this_also_has_to_support' \ - 'veryverylooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo' \ - 'ooooooooooooooooooooooooooooooooooooooongdirectorynames/%s' - f['file_path'] = file_path % f['file_name'] - f['identifier'] = 'abc123111' - - response = self.client.post('/rest/files', f, format="json") + file_path = ( + "/project_y_FROZEN/Experiment_1/path/of/lonely/file_and_this_also_has_to_support" + "veryverylooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo" + "ooooooooooooooooooooooooooooooooooooooongdirectorynames/%s" + ) + f["file_path"] = file_path % f["file_name"] + f["identifier"] = "abc123111" + + response = self.client.post("/rest/files", f, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('date_created' in response.data, True) - self.assertEqual('parent_directory' in response.data, True) + self.assertEqual("date_created" in response.data, True) + self.assertEqual("parent_directory" in response.data, True) - dirs_count = Directory.objects.filter(project_identifier='project_y').count() - dirs_created_count = self._count_dirs_from_path(f['file_path']) + dirs_count = Directory.objects.filter(project_identifier="project_y").count() + dirs_created_count = self._count_dirs_from_path(f["file_path"]) self.assertEqual(dirs_count, dirs_created_count) def test_create_file_append_to_existing_directory(self): @@ -428,24 +517,38 @@ def test_create_file_append_to_existing_directory(self): Note: Targeting project_x, which exists in pre-generated test data. """ - project_identifier = 'project_x' + project_identifier = "project_x" dir_count_before = Directory.objects.filter(project_identifier=project_identifier).count() - file_count_before = Directory.objects.filter(project_identifier=project_identifier, - directory_path='/project_x_FROZEN/Experiment_X/Phase_1').first().files.all().count() + file_count_before = ( + Directory.objects.filter( + project_identifier=project_identifier, + directory_path="/project_x_FROZEN/Experiment_X/Phase_1", + ) + .first() + .files.all() + .count() + ) f = self._form_complex_list_from_test_file()[0] - f['file_path'] = '/project_x_FROZEN/Experiment_X/Phase_1/%s' % f['file_name'] - f['identifier'] = '%s-111' % f['file_path'] - f['project_identifier'] = project_identifier + f["file_path"] = "/project_x_FROZEN/Experiment_X/Phase_1/%s" % f["file_name"] + f["identifier"] = "%s-111" % f["file_path"] + f["project_identifier"] = project_identifier - response = self.client.post('/rest/files', f, format="json") + response = self.client.post("/rest/files", f, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('date_created' in response.data, True) - self.assertEqual('parent_directory' in response.data, True) + self.assertEqual("date_created" in response.data, True) + self.assertEqual("parent_directory" in response.data, True) dir_count_after = Directory.objects.filter(project_identifier=project_identifier).count() - file_count_after = Directory.objects.filter(project_identifier=project_identifier, - directory_path='/project_x_FROZEN/Experiment_X/Phase_1').first().files.all().count() + file_count_after = ( + Directory.objects.filter( + project_identifier=project_identifier, + directory_path="/project_x_FROZEN/Experiment_X/Phase_1", + ) + .first() + .files.all() + .count() + ) self.assertEqual(dir_count_before, dir_count_after) self.assertEqual(file_count_after - file_count_before, 1) @@ -457,13 +560,13 @@ def test_create_file_hierarchy_from_file_list_with_no_existing_files(self): """ experiment_1_file_list = self._form_complex_list_from_test_file() - response = self.client.post('/rest/files', experiment_1_file_list, format="json") + response = self.client.post("/rest/files", experiment_1_file_list, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 12) - self.assertEqual(len(response.data['failed']), 0) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual(len(response.data["success"]), 12) + self.assertEqual(len(response.data["failed"]), 0) - dirs_dict = self._assert_directory_parent_dirs('project_y') + dirs_dict = self._assert_directory_parent_dirs("project_y") self._assert_file_parent_dirs(dirs_dict, response) def test_create_file_hierarchy_from_file_list_with_existing_files(self): @@ -480,23 +583,25 @@ def test_create_file_hierarchy_from_file_list_with_existing_files(self): # setup db to have pre-existing dirs experiment_1_file_list = self._form_complex_list_from_test_file() - response = self.client.post('/rest/files', experiment_1_file_list, format="json") + response = self.client.post("/rest/files", experiment_1_file_list, format="json") # form new test data experiment_2_file_list = self._form_complex_list_from_test_file() for i, f in enumerate(experiment_2_file_list): - f['file_path'] = f['file_path'].replace('/project_y_FROZEN/Experiment_1', - '/project_y_FROZEN/Experiment_2/Phase_1/Data') - f['identifier'] = '%s-%d' % (f['file_path'], i) + f["file_path"] = f["file_path"].replace( + "/project_y_FROZEN/Experiment_1", + "/project_y_FROZEN/Experiment_2/Phase_1/Data", + ) + f["identifier"] = "%s-%d" % (f["file_path"], i) - response = self.client.post('/rest/files', experiment_2_file_list, format="json") + response = self.client.post("/rest/files", experiment_2_file_list, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 12) - self.assertEqual(len(response.data['failed']), 0) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual(len(response.data["success"]), 12) + self.assertEqual(len(response.data["failed"]), 0) - dirs_dict = self._assert_directory_parent_dirs('project_y') + dirs_dict = self._assert_directory_parent_dirs("project_y") self._assert_file_parent_dirs(dirs_dict, response) def test_append_files_to_existing_directory(self): @@ -508,7 +613,7 @@ def test_append_files_to_existing_directory(self): # setup db to have pre-existing dirs experiment_1_file_list = self._form_complex_list_from_test_file() - response = self.client.post('/rest/files', experiment_1_file_list, format="json") + response = self.client.post("/rest/files", experiment_1_file_list, format="json") # form new test data, and trim it down a bit experiment_2_file_list = self._form_complex_list_from_test_file() @@ -516,16 +621,16 @@ def test_append_files_to_existing_directory(self): experiment_2_file_list.pop() for i, f in enumerate(experiment_2_file_list): - f['file_path'] = '/project_y_FROZEN/Experiment_2/%s' % f['file_name'] - f['identifier'] = '%s-%d' % (f['file_path'], i) + f["file_path"] = "/project_y_FROZEN/Experiment_2/%s" % f["file_name"] + f["identifier"] = "%s-%d" % (f["file_path"], i) - response = self.client.post('/rest/files', experiment_2_file_list, format="json") + response = self.client.post("/rest/files", experiment_2_file_list, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual(len(response.data['failed']), 0, response.data['failed']) - self.assertEqual(len(response.data['success']), 5) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual(len(response.data["failed"]), 0, response.data["failed"]) + self.assertEqual(len(response.data["success"]), 5) - dirs_dict = self._assert_directory_parent_dirs('project_y') + dirs_dict = self._assert_directory_parent_dirs("project_y") self._assert_file_parent_dirs(dirs_dict, response) def test_append_one_file_to_existing_directory(self): @@ -537,22 +642,22 @@ def test_append_one_file_to_existing_directory(self): # setup db to have pre-existing dirs experiment_1_file_list = self._form_complex_list_from_test_file() - response = self.client.post('/rest/files', experiment_1_file_list, format="json") + response = self.client.post("/rest/files", experiment_1_file_list, format="json") # form new test data, but use just the first item experiment_2_file_list = self._form_complex_list_from_test_file()[0:1] for i, f in enumerate(experiment_2_file_list): - f['file_path'] = '/project_y_FROZEN/Experiment_2/%s' % f['file_name'] - f['identifier'] = '%s-%d' % (f['file_path'], i) + f["file_path"] = "/project_y_FROZEN/Experiment_2/%s" % f["file_name"] + f["identifier"] = "%s-%d" % (f["file_path"], i) - response = self.client.post('/rest/files', experiment_2_file_list, format="json") + response = self.client.post("/rest/files", experiment_2_file_list, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 1) - self.assertEqual(len(response.data['failed']), 0) + self.assertEqual("success" in response.data.keys(), True) + self.assertEqual(len(response.data["success"]), 1) + self.assertEqual(len(response.data["failed"]), 0) - dirs_dict = self._assert_directory_parent_dirs('project_y') + dirs_dict = self._assert_directory_parent_dirs("project_y") self._assert_file_parent_dirs(dirs_dict, response) def test_create_file_hierarchy_error_file_list_has_invalid_data(self): @@ -562,33 +667,35 @@ def test_create_file_hierarchy_error_file_list_has_invalid_data(self): in a single request is also not permitted. """ experiment_1_file_list = self._form_complex_list_from_test_file() - experiment_1_file_list[0].pop('file_path') - response = self.client.post('/rest/files', experiment_1_file_list, format="json") + experiment_1_file_list[0].pop("file_path") + response = self.client.post("/rest/files", experiment_1_file_list, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_path' in response.data, True) - self.assertEqual('required parameter' in response.data['file_path'][0], True) + self.assertEqual("file_path" in response.data, True) + self.assertEqual("required parameter" in response.data["file_path"][0], True) experiment_1_file_list = self._form_complex_list_from_test_file() - experiment_1_file_list[0].pop('project_identifier') - response = self.client.post('/rest/files', experiment_1_file_list, format="json") + experiment_1_file_list[0].pop("project_identifier") + response = self.client.post("/rest/files", experiment_1_file_list, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('project_identifier' in response.data, True) - self.assertEqual('required parameter' in response.data['project_identifier'][0], True) + self.assertEqual("project_identifier" in response.data, True) + self.assertEqual("required parameter" in response.data["project_identifier"][0], True) experiment_1_file_list = self._form_complex_list_from_test_file() - experiment_1_file_list[0]['project_identifier'] = 'second_project' - response = self.client.post('/rest/files', experiment_1_file_list, format="json") + experiment_1_file_list[0]["project_identifier"] = "second_project" + response = self.client.post("/rest/files", experiment_1_file_list, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('project_identifier' in response.data, True) - self.assertEqual('multiple projects' in response.data['project_identifier'][0], True) + self.assertEqual("project_identifier" in response.data, True) + self.assertEqual("multiple projects" in response.data["project_identifier"][0], True) def test_filepath_starts_with_slash(self): file = self._get_new_test_data() - file['file_path'] = file['file_path'][1:] + file["file_path"] = file["file_path"][1:] - response = self.client.post('/rest/files', file, format="json") + response = self.client.post("/rest/files", file, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue("file path should start with '/' to point to the root" in response.data['file_path'][0]) + self.assertTrue( + "file path should start with '/' to point to the root" in response.data["file_path"][0] + ) def _assert_directory_parent_dirs(self, project_identifier): """ @@ -598,17 +705,24 @@ def _assert_directory_parent_dirs(self, project_identifier): for d in Directory.objects.filter(project_identifier=project_identifier): dirs_dict[d.directory_path] = { - 'dir_id': d.id, - 'parent_dir_id': d.parent_directory and d.parent_directory.id or None + "dir_id": d.id, + "parent_dir_id": d.parent_directory and d.parent_directory.id or None, } for dir_path, ids in dirs_dict.items(): - if dir_path == '/': - self.assertEqual(ids['parent_dir_id'], None, 'root dir \'/\' should not have a parent directory') + if dir_path == "/": + self.assertEqual( + ids["parent_dir_id"], + None, + "root dir '/' should not have a parent directory", + ) continue expected_parent_dir_path = dirname(dir_path) - self.assertEqual(ids['parent_dir_id'], dirs_dict[expected_parent_dir_path]['dir_id'], - 'parent dir not as expected.') + self.assertEqual( + ids["parent_dir_id"], + dirs_dict[expected_parent_dir_path]["dir_id"], + "parent dir not as expected.", + ) return dirs_dict @@ -616,11 +730,14 @@ def _assert_file_parent_dirs(self, dirs_dict, response): """ Check files have parent dirs as expected. """ - for entry in response.data['success']: - f = entry['object'] - excpected_parent_dir_path = dirname(f['file_path']) - self.assertEqual(f['parent_directory']['id'], dirs_dict[excpected_parent_dir_path]['dir_id'], - 'parent dir not as expected.') + for entry in response.data["success"]: + f = entry["object"] + excpected_parent_dir_path = dirname(f["file_path"]) + self.assertEqual( + f["parent_directory"]["id"], + dirs_dict[excpected_parent_dir_path]["dir_id"], + "parent dir not as expected.", + ) def _form_complex_list_from_test_file(self): """ @@ -678,18 +795,20 @@ def _form_complex_list_from_test_file(self): ] template = self.test_new_data - template.pop('id', None) - template.pop('identifier', None) - template.pop('project_identifier', None) - template.pop('parent_directory', None) - template.pop('date_created', None) - template.pop('date_modified', None) - template.pop('service_created', None) + template.pop("id", None) + template.pop("identifier", None) + template.pop("project_identifier", None) + template.pop("parent_directory", None) + template.pop("date_created", None) + template.pop("date_modified", None) + template.pop("service_created", None) files = [] for i, d in enumerate(dir_data): files.append(deepcopy(template)) - files[-1].update(d, identifier='pid:urn:test:file:%d' % i, project_identifier='project_y') + files[-1].update( + d, identifier="pid:urn:test:file:%d" % i, project_identifier="project_y" + ) return files @@ -700,15 +819,15 @@ class FileApiWriteUpdateTests(FileApiWriteCommon): """ def test_update_file(self): - f = self.client.get('/rest/files/1').data - f['file_format'] = 'csv' - response = self.client.put('/rest/files/%s' % f['identifier'], f, format="json") + f = self.client.get("/rest/files/1").data + f["file_format"] = "csv" + response = self.client.put("/rest/files/%s" % f["identifier"], f, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) def test_prevent_file_path_update_after_create(self): - f = self.client.get('/rest/files/1').data - f['file_path'] = '%s_bak' % f['file_path'] - response = self.client.put('/rest/files/%s' % f['identifier'], f, format="json") + f = self.client.get("/rest/files/1").data + f["file_path"] = "%s_bak" % f["file_path"] + response = self.client.put("/rest/files/%s" % f["identifier"], f, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_update_file_error_required_fields(self): @@ -716,120 +835,151 @@ def test_update_file_error_required_fields(self): Field 'project_identifier' is missing, which should result in an error, since PUT replaces an object and requires all 'required' fields to be present. """ - self.test_new_data.pop('project_identifier') - response = self.client.put('/rest/files/%s' % self.identifier, self.test_new_data, format="json") + self.test_new_data.pop("project_identifier") + response = self.client.put( + "/rest/files/%s" % self.identifier, self.test_new_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('project_identifier' in response.data.keys(), True, - 'Error for field \'project_identifier\' is missing from response.data') + self.assertEqual( + "project_identifier" in response.data.keys(), + True, + "Error for field 'project_identifier' is missing from response.data", + ) def test_update_file_not_found(self): - response = self.client.put('/rest/files/doesnotexist', self.test_new_data, format="json") + response = self.client.put("/rest/files/doesnotexist", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_update_file_allowed_projects_ok(self): - f = self.client.get('/rest/files/1').data - response = self.client.put('/rest/files/%s?allowed_projects=%s' % (f['identifier'], f['project_identifier']), - f, format="json") + f = self.client.get("/rest/files/1").data + response = self.client.put( + "/rest/files/%s?allowed_projects=%s" % (f["identifier"], f["project_identifier"]), + f, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_update_file_allowed_projects_fail(self): - f = self.client.get('/rest/files/1').data - response = self.client.put('/rest/files/%s?allowed_projects=nopermission' % f['identifier'], f, format="json") + f = self.client.get("/rest/files/1").data + response = self.client.put( + "/rest/files/%s?allowed_projects=nopermission" % f["identifier"], + f, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_update_file_allowed_projects_not_dict(self): - f = self.client.get('/rest/files/1').data - response = self.client.put('/rest/files/%s?allowed_projects=%s' % (f['identifier'], f['project_identifier']), - [f], format="json") + f = self.client.get("/rest/files/1").data + response = self.client.put( + "/rest/files/%s?allowed_projects=%s" % (f["identifier"], f["project_identifier"]), + [f], + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('json' in response.data['detail'][0], True, 'Error regarding datatype') + self.assertEqual("json" in response.data["detail"][0], True, "Error regarding datatype") # # update list operations PUT # def test_file_update_list(self): - f1 = self.client.get('/rest/files/1').data - f2 = self.client.get('/rest/files/2').data - new_file_format = 'changed-format' - new_file_format_2 = 'changed-format-2' - f1['file_format'] = new_file_format - f2['file_format'] = new_file_format_2 - - response = self.client.put('/rest/files', [f1, f2], format="json") + f1 = self.client.get("/rest/files/1").data + f2 = self.client.get("/rest/files/2").data + new_file_format = "changed-format" + new_file_format_2 = "changed-format-2" + f1["file_format"] = new_file_format + f2["file_format"] = new_file_format_2 + + response = self.client.put("/rest/files", [f1, f2], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) updated_file = File.objects.get(pk=1) self.assertEqual(updated_file.file_format, new_file_format) def test_file_update_list_error_one_fails(self): - f1 = self.client.get('/rest/files/1').data - f2 = self.client.get('/rest/files/2').data - new_file_format = 'changed-format' - f1['file_format'] = new_file_format + f1 = self.client.get("/rest/files/1").data + f2 = self.client.get("/rest/files/2").data + new_file_format = "changed-format" + f1["file_format"] = new_file_format # cant be null - should fail - f2['file_frozen'] = None + f2["file_frozen"] = None - response = self.client.put('/rest/files', [f1, f2], format="json") + response = self.client.put("/rest/files", [f1, f2], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['success']), 1, 'success list should be empty') - self.assertEqual(len(response.data['failed']), 1, 'there should have been one failed element') - self.assertEqual('file_frozen' in response.data['failed'][0]['errors'], True, - 'error should be about file_characteristics missing') + self.assertEqual(len(response.data["success"]), 1, "success list should be empty") + self.assertEqual( + len(response.data["failed"]), 1, "there should have been one failed element" + ) + self.assertEqual( + "file_frozen" in response.data["failed"][0]["errors"], + True, + "error should be about file_characteristics missing", + ) updated_file = File.objects.get(pk=1) self.assertEqual(updated_file.file_format, new_file_format) def test_file_update_list_error_key_not_found(self): - f1 = self.client.get('/rest/files/1').data - f2 = self.client.get('/rest/files/2').data - new_file_format = 'changed-format' - new_file_format_2 = 'changed-format-2' - f1['file_format'] = new_file_format - f2['file_format'] = new_file_format_2 + f1 = self.client.get("/rest/files/1").data + f2 = self.client.get("/rest/files/2").data + new_file_format = "changed-format" + new_file_format_2 = "changed-format-2" + f1["file_format"] = new_file_format + f2["file_format"] = new_file_format_2 # has no lookup key - should fail - f2.pop('id') - f2.pop('identifier') + f2.pop("id") + f2.pop("identifier") - response = self.client.put('/rest/files', [f1, f2], format="json") + response = self.client.put("/rest/files", [f1, f2], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['success']), 1, 'success list should be empty') - self.assertEqual(len(response.data['failed']), 1, 'there should have been one failed element') - error_msg_of_failed_row = response.data['failed'][0]['errors']['detail'][0] - self.assertEqual('identifying keys' in error_msg_of_failed_row, True, - 'error should be about identifying keys missing') + self.assertEqual(len(response.data["success"]), 1, "success list should be empty") + self.assertEqual( + len(response.data["failed"]), 1, "there should have been one failed element" + ) + error_msg_of_failed_row = response.data["failed"][0]["errors"]["detail"][0] + self.assertEqual( + "identifying keys" in error_msg_of_failed_row, + True, + "error should be about identifying keys missing", + ) updated_file = File.objects.get(pk=1) self.assertEqual(updated_file.file_format, new_file_format) def test_file_update_list_allowed_projects_ok(self): # Both files in project 'project_x' - f1 = self.client.get('/rest/files/1').data - f2 = self.client.get('/rest/files/2').data + f1 = self.client.get("/rest/files/1").data + f2 = self.client.get("/rest/files/2").data - response = self.client.put('/rest/files?allowed_projects=project_x,y,z', [f1, f2], format="json") + response = self.client.put( + "/rest/files?allowed_projects=project_x,y,z", [f1, f2], format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) def test_file_update_list_allowed_projects_fail(self): # Files in projects 'project_x' and 'research_project_112' - f1 = self.client.get('/rest/files/1').data - f2 = self.client.get('/rest/files/39').data + f1 = self.client.get("/rest/files/1").data + f2 = self.client.get("/rest/files/39").data - response = self.client.put('/rest/files?allowed_projects=project_x,y,z', [f1, f2], format="json") + response = self.client.put( + "/rest/files?allowed_projects=project_x,y,z", [f1, f2], format="json" + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) def test_file_update_list_allowed_projects_empty_value(self): - f1 = self.client.get('/rest/files/1').data - response = self.client.put('/rest/files?allowed_projects=', [f1], format="json") + f1 = self.client.get("/rest/files/1").data + response = self.client.put("/rest/files?allowed_projects=", [f1], format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) def test_file_update_list_allowed_projects_not_list(self): new_data_1 = {} - new_data_1['identifier'] = "pid:urn:1" - new_data_1['file_name'] = 'Nice_new_name' + new_data_1["identifier"] = "pid:urn:1" + new_data_1["file_name"] = "Nice_new_name" - res = self.client.patch('/rest/files?allowed_projects=y,z,project_x', new_data_1, format="json") + res = self.client.patch( + "/rest/files?allowed_projects=y,z,project_x", new_data_1, format="json" + ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST, res.data) @@ -842,29 +992,43 @@ def test_update_file_partial(self): new_data = { "file_name": "new_file_name", } - response = self.client.patch('/rest/files/%s' % self.identifier, new_data, format="json") + response = self.client.patch("/rest/files/%s" % self.identifier, new_data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('file_name' in response.data.keys(), True) - self.assertEqual('file_path' in response.data.keys(), True, 'PATCH operation should return full content') - self.assertEqual(response.data['file_name'], 'new_file_name', 'Field file_name was not updated') + self.assertEqual("file_name" in response.data.keys(), True) + self.assertEqual( + "file_path" in response.data.keys(), + True, + "PATCH operation should return full content", + ) + self.assertEqual( + response.data["file_name"], + "new_file_name", + "Field file_name was not updated", + ) def test_update_partial_allowed_projects_ok(self): new_data = { "file_name": "new_file_name", } - response = self.client.patch('/rest/files/%s?allowed_projects=%s' % (self.identifier, self.pidentifier), - new_data, format="json") + response = self.client.patch( + "/rest/files/%s?allowed_projects=%s" % (self.identifier, self.pidentifier), + new_data, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['file_name'], 'new_file_name', response.data) + self.assertEqual(response.data["file_name"], "new_file_name", response.data) def test_update_partial_allowed_projects_fail(self): new_data = { "file_name": "new_file_name", } - response = self.client.patch('/rest/files/%s?allowed_projects=noproject' % self.identifier, - new_data, format="json") + response = self.client.patch( + "/rest/files/%s?allowed_projects=noproject" % self.identifier, + new_data, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) @@ -872,76 +1036,106 @@ def test_update_partial_allowed_projects_not_dict(self): new_data = { "file_name": "new_file_name", } - response = self.client.patch('/rest/files/%s?allowed_projects=%s' % (self.identifier, self.pidentifier), - [new_data], format="json") + response = self.client.patch( + "/rest/files/%s?allowed_projects=%s" % (self.identifier, self.pidentifier), + [new_data], + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('json' in response.data['detail'][0], True, 'Error regarding datatype') + self.assertEqual("json" in response.data["detail"][0], True, "Error regarding datatype") # # update list operations PATCH # def test_file_partial_update_list(self): - new_project_identifier = 'changed-project-identifier' - new_project_identifier_2 = 'changed-project-identifier-2' + new_project_identifier = "changed-project-identifier" + new_project_identifier_2 = "changed-project-identifier-2" test_data = {} - test_data['id'] = 1 - test_data['project_identifier'] = new_project_identifier + test_data["id"] = 1 + test_data["project_identifier"] = new_project_identifier second_test_data = {} - second_test_data['id'] = 2 - second_test_data['project_identifier'] = new_project_identifier_2 + second_test_data["id"] = 2 + second_test_data["project_identifier"] = new_project_identifier_2 - response = self.client.patch('/rest/files', [test_data, second_test_data], format="json") + response = self.client.patch("/rest/files", [test_data, second_test_data], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data, True, 'response.data should contain list of changed objects') - self.assertEqual(len(response.data['success']), 2, 'response.data should contain 2 changed objects') - self.assertEqual('file_characteristics' in response.data['success'][0]['object'], True, - 'response.data should contain full objects') + self.assertEqual( + "success" in response.data, + True, + "response.data should contain list of changed objects", + ) + self.assertEqual( + len(response.data["success"]), + 2, + "response.data should contain 2 changed objects", + ) + self.assertEqual( + "file_characteristics" in response.data["success"][0]["object"], + True, + "response.data should contain full objects", + ) updated_file = File.objects.get(pk=1) - self.assertEqual(updated_file.project_identifier, new_project_identifier, 'project_identifier did not update') + self.assertEqual( + updated_file.project_identifier, + new_project_identifier, + "project_identifier did not update", + ) def test_file_partial_update_list_allowed_projects_ok(self): new_data_1 = {} - new_data_1['identifier'] = "pid:urn:1" - new_data_1['file_name'] = 'Nice_new_name' + new_data_1["identifier"] = "pid:urn:1" + new_data_1["file_name"] = "Nice_new_name" new_data_2 = {} - new_data_2['identifier'] = "pid:urn:2" - new_data_2['file_name'] = 'Not_so_nice_name' - - res = self.client.patch('/rest/files?allowed_projects=y,z,project_x', [new_data_1, new_data_2], format="json") + new_data_2["identifier"] = "pid:urn:2" + new_data_2["file_name"] = "Not_so_nice_name" + + res = self.client.patch( + "/rest/files?allowed_projects=y,z,project_x", + [new_data_1, new_data_2], + format="json", + ) self.assertEqual(res.status_code, status.HTTP_200_OK, res.data) - self.assertEqual(res.data['success'][0]['object']['file_name'], 'Nice_new_name', res.data) + self.assertEqual(res.data["success"][0]["object"]["file_name"], "Nice_new_name", res.data) def test_file_partial_update_list_allowed_projects_fail(self): # Files in projects 'project_x' and 'research_project_112' - f1 = self.client.get('/rest/files/1').data - f2 = self.client.get('/rest/files/39').data + f1 = self.client.get("/rest/files/1").data + f2 = self.client.get("/rest/files/39").data - response = self.client.patch('/rest/files?allowed_projects=project_x,y,z', [f1, f2], format="json") + response = self.client.patch( + "/rest/files?allowed_projects=project_x,y,z", [f1, f2], format="json" + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) def test_file_partial_update_list_allowed_projects_not_list(self): new_data_1 = {} - new_data_1['identifier'] = "pid:urn:1" - new_data_1['file_name'] = 'Nice_new_name' + new_data_1["identifier"] = "pid:urn:1" + new_data_1["file_name"] = "Nice_new_name" - res = self.client.patch('/rest/files?allowed_projects=y,z,project_x', new_data_1, format="json") + res = self.client.patch( + "/rest/files?allowed_projects=y,z,project_x", new_data_1, format="json" + ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST, res.data) def test_file_partial_update_list_allowed_projects_no_identifier(self): new_data_1 = {} - new_data_1['file_name'] = 'Nice_new_name' + new_data_1["file_name"] = "Nice_new_name" new_data_2 = {} - new_data_2['id'] = 23 - new_data_2['file_name'] = 'Not_so_nice_name' - - res = self.client.patch('/rest/files?allowed_projects=y,z,project_x', [new_data_1, new_data_2], format="json") + new_data_2["id"] = 23 + new_data_2["file_name"] = "Not_so_nice_name" + + res = self.client.patch( + "/rest/files?allowed_projects=y,z,project_x", + [new_data_1, new_data_2], + format="json", + ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST, res.data) @@ -956,34 +1150,44 @@ class FileApiWriteDeleteTests(FileApiWriteCommon): def test_delete_single_file_ok(self): dir_count_before = Directory.objects.all().count() - response = self.client.delete('/rest/files/1') + response = self.client.delete("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('deleted_files_count' in response.data, True, response.data) - self.assertEqual(response.data['deleted_files_count'], 1, response.data) + self.assertEqual("deleted_files_count" in response.data, True, response.data) + self.assertEqual(response.data["deleted_files_count"], 1, response.data) dir_count_after = Directory.objects.all().count() - self.assertEqual(dir_count_before, dir_count_after, 'no dirs should have been deleted') + self.assertEqual(dir_count_before, dir_count_after, "no dirs should have been deleted") deleted_file = File.objects_unfiltered.get(pk=1) self._check_project_root_byte_size_and_file_count(deleted_file.project_identifier) - self.assertEqual(deleted_file.date_modified, deleted_file.file_deleted, 'date_modified should be updated') + self.assertEqual( + deleted_file.date_modified, + deleted_file.file_deleted, + "date_modified should be updated", + ) def test_delete_single_file_ok_destroy_leading_dirs(self): - project_identifier = 'project_z' + project_identifier = "project_z" test_data = deepcopy(self.test_new_data) - test_data['file_path'] = '/project_z/some/path/here/%s' % test_data['file_name'] - test_data['project_identifier'] = project_identifier - test_data['identifier'] = 'abc123' - response = self.client.post('/rest/files', test_data, format='json') - self.assertEqual(Directory.objects.filter(project_identifier=project_identifier).exists(), True) - - response = self.client.delete('/rest/files/%s' % response.data['id']) + test_data["file_path"] = "/project_z/some/path/here/%s" % test_data["file_name"] + test_data["project_identifier"] = project_identifier + test_data["identifier"] = "abc123" + response = self.client.post("/rest/files", test_data, format="json") + self.assertEqual( + Directory.objects.filter(project_identifier=project_identifier).exists(), + True, + ) + + response = self.client.delete("/rest/files/%s" % response.data["id"]) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('deleted_files_count' in response.data, True, response.data) - self.assertEqual(response.data['deleted_files_count'], 1, response.data) + self.assertEqual("deleted_files_count" in response.data, True, response.data) + self.assertEqual(response.data["deleted_files_count"], 1, response.data) - self.assertEqual(Directory.objects.filter(project_identifier=project_identifier).exists(), False) + self.assertEqual( + Directory.objects.filter(project_identifier=project_identifier).exists(), + False, + ) def test_delete_single_file_404(self): - response = self.client.delete('/rest/files/doesnotexist') + response = self.client.delete("/rest/files/doesnotexist") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_bulk_delete_files_identifiers_not_found(self): @@ -991,8 +1195,8 @@ def test_bulk_delete_files_identifiers_not_found(self): A bulk delete request to /files, but any of the identifiers provided are not found. Should return 404. """ - identifiers = ['nope', 'doesnotexist', 'stillno'] - response = self.client.delete('/rest/files', identifiers, format="json") + identifiers = ["nope", "doesnotexist", "stillno"] + response = self.client.delete("/rest/files", identifiers, format="json") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) def test_bulk_delete_files_some_identifiers_not_found(self): @@ -1001,13 +1205,15 @@ def test_bulk_delete_files_some_identifiers_not_found(self): Should be ok, delete those files that are found. Assumably those identifiers that were not found did not exist anyway, therefore no harm is done. """ - identifiers = ['nope', 'doesnotexist', 'stillno'] + identifiers = ["nope", "doesnotexist", "stillno"] identifiers.append(File.objects.get(pk=1).identifier) - response = self.client.delete('/rest/files', identifiers, format="json") + response = self.client.delete("/rest/files", identifiers, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) removed = File.objects_unfiltered.get(pk=1).removed - self.assertEqual(removed, True, 'file should have been removed') - self._check_project_root_byte_size_and_file_count(File.objects_unfiltered.get(pk=1).project_identifier) + self.assertEqual(removed, True, "file should have been removed") + self._check_project_root_byte_size_and_file_count( + File.objects_unfiltered.get(pk=1).project_identifier + ) def test_bulk_delete_files_in_single_directory_1(self): """ @@ -1020,7 +1226,7 @@ def test_bulk_delete_files_in_single_directory_1(self): all_files_count_before = File.objects.all().count() file_ids = [f.id for f in Directory.objects.get(pk=3).files.all()] - response = self.client.delete('/rest/files', file_ids, format="json") + response = self.client.delete("/rest/files", file_ids, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) all_files_count_after = File.objects.all().count() @@ -1033,7 +1239,7 @@ def test_bulk_delete_files_in_single_directory_2(self): all_files_count_before = File.objects.all().count() file_ids = [f.id for f in Directory.objects.get(pk=4).files.all()] - response = self.client.delete('/rest/files', file_ids, format="json") + response = self.client.delete("/rest/files", file_ids, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) all_files_count_after = File.objects.all().count() @@ -1044,20 +1250,20 @@ def test_bulk_delete_file_list_one_file_id_missing(self): Otherwise complete set of files, but from one dir one file is missing. Should leave the one file intact, while preserving the directory tree. """ - all_files_count_before = File.objects.filter(project_identifier='project_x').count() - file_ids = [f.id for f in File.objects.filter(project_identifier='project_x')] + all_files_count_before = File.objects.filter(project_identifier="project_x").count() + file_ids = [f.id for f in File.objects.filter(project_identifier="project_x")] # everything except the last file should be removed file_ids.pop() - response = self.client.delete('/rest/files', file_ids, format="json") + response = self.client.delete("/rest/files", file_ids, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - all_files_after = File.objects.filter(project_identifier='project_x') + all_files_after = File.objects.filter(project_identifier="project_x") self.assertEqual(all_files_after.count(), all_files_count_before - len(file_ids)) expected_dirs_count = self._count_dirs_from_path(all_files_after[0].file_path) - actual_dirs_count = Directory.objects.filter(project_identifier='project_x').count() + actual_dirs_count = Directory.objects.filter(project_identifier="project_x").count() self.assertEqual(actual_dirs_count, expected_dirs_count) def test_bulk_delete_files_from_root(self): @@ -1066,16 +1272,23 @@ def test_bulk_delete_files_from_root(self): so the whole tree should end up being deleted. """ files_to_remove_count = 20 - file_ids = File.objects.filter(project_identifier='project_x').values_list('id', flat=True) + file_ids = File.objects.filter(project_identifier="project_x").values_list("id", flat=True) self.assertEqual(len(file_ids), files_to_remove_count) - response = self.client.delete('/rest/files', file_ids, format="json") + response = self.client.delete("/rest/files", file_ids, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('deleted_files_count', None), files_to_remove_count, response.data) - - self._assert_files_available_and_removed('project_x', 0, files_to_remove_count) - self.assertEqual(Directory.objects_unfiltered.filter(project_identifier='project_x').count(), 0, - 'all dirs should have been permanently removed') + self.assertEqual( + response.data.get("deleted_files_count", None), + files_to_remove_count, + response.data, + ) + + self._assert_files_available_and_removed("project_x", 0, files_to_remove_count) + self.assertEqual( + Directory.objects_unfiltered.filter(project_identifier="project_x").count(), + 0, + "all dirs should have been permanently removed", + ) def test_bulk_delete_sub_directory_1(self): """ @@ -1087,17 +1300,21 @@ def test_bulk_delete_sub_directory_1(self): file_ids += [f.id for f in Directory.objects.get(pk=6).files.all()] self.assertEqual(len(file_ids), files_to_remove_count) - response = self.client.delete('/rest/files', file_ids, format="json") + response = self.client.delete("/rest/files", file_ids, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('deleted_files_count', None), files_to_remove_count, response.data) + self.assertEqual( + response.data.get("deleted_files_count", None), + files_to_remove_count, + response.data, + ) - self._assert_files_available_and_removed('project_x', 5, files_to_remove_count) + self._assert_files_available_and_removed("project_x", 5, files_to_remove_count) # these dirs should still be left: # / # /project_x_FROZEN # /project_x_FROZEN/Experiment_X (has 5 files) - self.assertEqual(Directory.objects.filter(project_identifier='project_x').count(), 3) + self.assertEqual(Directory.objects.filter(project_identifier="project_x").count(), 3) def test_bulk_delete_sub_directory_2(self): """ @@ -1108,36 +1325,51 @@ def test_bulk_delete_sub_directory_2(self): file_ids = [f.id for f in Directory.objects.get(pk=6).files.all()] self.assertEqual(len(file_ids), files_to_remove_count) - response = self.client.delete('/rest/files', file_ids, format="json") + response = self.client.delete("/rest/files", file_ids, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('deleted_files_count', None), files_to_remove_count, response.data) + self.assertEqual( + response.data.get("deleted_files_count", None), + files_to_remove_count, + response.data, + ) - self._assert_files_available_and_removed('project_x', 10, files_to_remove_count) + self._assert_files_available_and_removed("project_x", 10, files_to_remove_count) # these dirs should still be left: # / # /project_x_FROZEN # /project_x_FROZEN/Experiment_X (5 files) # /project_x_FROZEN/Experiment_X/Phase_1 (5 files) - self.assertEqual(Directory.objects.filter(project_identifier='project_x').count(), 4) + self.assertEqual(Directory.objects.filter(project_identifier="project_x").count(), 4) # /project_x_FROZEN/Experiment_X/Phase_1/2017 <- this dir should be deleted, since # it only contained the 01-dir, which we specifically targeted for deletion - self.assertEqual(Directory.objects.filter( - project_identifier='project_x', - directory_path='/project_x_FROZEN/Experiment_X/Phase_1/2017' - ).count(), 0, 'dir should have been deleted') + self.assertEqual( + Directory.objects.filter( + project_identifier="project_x", + directory_path="/project_x_FROZEN/Experiment_X/Phase_1/2017", + ).count(), + 0, + "dir should have been deleted", + ) def _assert_files_available_and_removed(self, project_identifier, available, removed): """ After deleting files, check qty of files retrievable by usual means is as expected, and qty of files retrievable from objects_unfiltered with removed=True is as expected. """ - self.assertEqual(File.objects.filter(project_identifier=project_identifier).count(), available, - 'files should not be retrievable from removed=False scope') - self.assertEqual(File.objects_unfiltered.filter(project_identifier=project_identifier, removed=True).count(), - removed, - 'files should be retrievable from removed=True scope') + self.assertEqual( + File.objects.filter(project_identifier=project_identifier).count(), + available, + "files should not be retrievable from removed=False scope", + ) + self.assertEqual( + File.objects_unfiltered.filter( + project_identifier=project_identifier, removed=True + ).count(), + removed, + "files should be retrievable from removed=True scope", + ) def test_deleting_files_deprecates_datasets(self): for cr in CatalogRecord.objects.filter(deprecated=True): @@ -1146,33 +1378,37 @@ def test_deleting_files_deprecates_datasets(self): cr.force_save() datasets_with_file = CatalogRecord.objects.filter(files__id=1).count() - response = self.client.delete('/rest/files/1') + response = self.client.delete("/rest/files/1") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(CatalogRecord.objects.filter(deprecated=True).count(), datasets_with_file) class FileApiWriteRestoreTests(FileApiWriteCommon): - def test_restore_files_ok(self): """ Restore a few deleted files from directories, that still contain other files. Restored files should be appended to previously existing files. """ - response = self.client.delete('/rest/files/1') - response = self.client.delete('/rest/files/2') - response = self.client.delete('/rest/files/3') + response = self.client.delete("/rest/files/1") + response = self.client.delete("/rest/files/2") + response = self.client.delete("/rest/files/3") self.assertEqual(response.status_code, status.HTTP_200_OK) - deleted_files = File.objects_unfiltered.filter(pk__in=[1, 2, 3]) \ - .values('identifier', 'parent_directory_id') + deleted_files = File.objects_unfiltered.filter(pk__in=[1, 2, 3]).values( + "identifier", "parent_directory_id" + ) - response = self.client.post('/rest/files/restore', [f['identifier'] for f in deleted_files], format='json') + response = self.client.post( + "/rest/files/restore", + [f["identifier"] for f in deleted_files], + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('restored_files_count' in response.data, True, response.data) - self.assertEqual(response.data['restored_files_count'], 3, response.data) + self.assertEqual("restored_files_count" in response.data, True, response.data) + self.assertEqual(response.data["restored_files_count"], 3, response.data) # ensure restored files are using previously existing directories - old_parent_dirs = { f['parent_directory_id'] for f in deleted_files } + old_parent_dirs = {f["parent_directory_id"] for f in deleted_files} files = File.objects.filter(pk__in=[1, 2, 3]) for f in files: self.assertEqual(f.file_deleted, None) @@ -1185,20 +1421,25 @@ def test_restore_files_recreate_missing_directories(self): """ proj = File.objects.get(pk=1).project_identifier - response = self.client.get('/rest/files?project_identifier=%s&fields=identifier&no_pagination=true' - % proj, format='json') - file_identifiers = [ f['identifier'] for f in response.data ] + response = self.client.get( + "/rest/files?project_identifier=%s&fields=identifier&no_pagination=true" % proj, + format="json", + ) + file_identifiers = [f["identifier"] for f in response.data] - self.client.delete('/rest/files', file_identifiers, format='json') + self.client.delete("/rest/files", file_identifiers, format="json") - deleted_directory_ids = File.objects_unfiltered.filter(identifier__in=file_identifiers) \ - .values_list('parent_directory_id', flat=True) - old_parent_dirs = { id for id in deleted_directory_ids } + deleted_directory_ids = File.objects_unfiltered.filter( + identifier__in=file_identifiers + ).values_list("parent_directory_id", flat=True) + old_parent_dirs = {id for id in deleted_directory_ids} - response = self.client.post('/rest/files/restore', file_identifiers, format='json') + response = self.client.post("/rest/files/restore", file_identifiers, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('restored_files_count' in response.data, True, response.data) - self.assertEqual(response.data['restored_files_count'], len(file_identifiers), response.data) + self.assertEqual("restored_files_count" in response.data, True, response.data) + self.assertEqual( + response.data["restored_files_count"], len(file_identifiers), response.data + ) # ensure restored files are using new directories files = File.objects.filter(identifier__in=file_identifiers) @@ -1206,15 +1447,21 @@ def test_restore_files_recreate_missing_directories(self): self.assertEqual(f.parent_directory_id in old_parent_dirs, False) def test_check_parameter_is_string_list(self): - response = self.client.post('/rest/files/restore', ['a', 'b', 1], format='json') + response = self.client.post("/rest/files/restore", ["a", "b", 1], format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_check_files_belong_to_one_project(self): f1 = File.objects_unfiltered.get(pk=1) - f2 = File.objects_unfiltered.filter().exclude(project_identifier=f1.project_identifier).first() - response = self.client.delete('/rest/files/%d' % f1.id) - response = self.client.delete('/rest/files/%d' % f2.id) - response = self.client.post('/rest/files/restore', [ f1.identifier, f2.identifier ], format='json') + f2 = ( + File.objects_unfiltered.filter() + .exclude(project_identifier=f1.project_identifier) + .first() + ) + response = self.client.delete("/rest/files/%d" % f1.id) + response = self.client.delete("/rest/files/%d" % f2.id) + response = self.client.post( + "/rest/files/restore", [f1.identifier, f2.identifier], format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -1224,52 +1471,66 @@ class FileApiWriteXmlTests(FileApiWriteCommon): """ def test_xml_api(self): - content_type = 'application/xml' - data = 'tauhketta yeah' + content_type = "application/xml" + data = ( + 'tauhketta yeah' + ) # create - response = self.client.post('/rest/files/1/xml?namespace=breh', data, content_type=content_type) + response = self.client.post( + "/rest/files/1/xml?namespace=breh", data, content_type=content_type + ) self.assertEqual(response.status_code in (200, 201, 204), True) - self.assertEqual('updated stuff' - response = self.client.put('/rest/files/1/xml?namespace=breh', data, content_type=content_type) + response = self.client.put( + "/rest/files/1/xml?namespace=breh", data, content_type=content_type + ) self.assertEqual(response.status_code in (200, 201, 204), True) # get updated again - response = self.client.get('/rest/files/1/xml?namespace=breh', content_type=content_type, ) - self.assertEqual('updated stuff' in response.data, True) + response = self.client.get( + "/rest/files/1/xml?namespace=breh", + content_type=content_type, + ) + self.assertEqual("updated stuff" in response.data, True) # delete - response = self.client.delete('/rest/files/1/xml?namespace=breh', data, content_type=content_type) + response = self.client.delete( + "/rest/files/1/xml?namespace=breh", data, content_type=content_type + ) self.assertEqual(response.status_code in (200, 201, 204), True) - response = self.client.delete('/rest/files/1/xml?namespace=bruh', data, content_type=content_type) + response = self.client.delete( + "/rest/files/1/xml?namespace=bruh", data, content_type=content_type + ) self.assertEqual(response.status_code in (200, 201, 204), True) # get list - response = self.client.get('/rest/files/1/xml', content_type=content_type) + response = self.client.get("/rest/files/1/xml", content_type=content_type) self.assertEqual(response.status_code in (200, 201, 204), True) class FileApiWriteEndUserAccess(FileApiWriteCommon): - def setUp(self): super().setUp() self.token = get_test_oidc_token() @@ -1277,110 +1538,108 @@ def setUp(self): @responses.activate def test_user_cant_create_files(self): - ''' + """ Ensure users are unable to create new files. - ''' + """ # ensure user belongs to same project - self.token['group_names'].append('IDA01:%s' % self.test_new_data['project_identifier']) - self._use_http_authorization(method='bearer', token=self.token) + self.token["group_names"].append("IDA01:%s" % self.test_new_data["project_identifier"]) + self._use_http_authorization(method="bearer", token=self.token) - response = self.client.post('/rest/files', self.test_new_data, format="json") + response = self.client.post("/rest/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @responses.activate def test_user_can_only_update_permitted_file_fields(self): - ''' + """ Ensure users are only able to modify permitted fields. - ''' + """ # ensure user belongs to same project proj = File.objects.get(pk=1).project_identifier - self.token['group_names'].append('IDA01:%s' % proj) - self._use_http_authorization(method='bearer', token=self.token) + self.token["group_names"].append("IDA01:%s" % proj) + self._use_http_authorization(method="bearer", token=self.token) - response = self.client.get('/rest/files/1', format="json") + response = self.client.get("/rest/files/1", format="json") file = response.data original_file = deepcopy(file) - file['byte_size'] = 200 - file['checksum']['value'] = 'changed' - file['parent_directory'] = 1 - file['file_frozen'] = '3' + file['file_frozen'][1:] - file['file_format'] = 'changed' - file['file_name'] = 'changed' - file['file_path'] = '/oh/no' - file['file_storage'] = 2 - file['file_uploaded'] = '3' + file['file_uploaded'][1:] - file['identifier'] = 'changed' - file['open_access'] = True - file['project_identifier'] = 'changed' - file['service_modified'] = 'changed' - file['service_created'] = 'changed' - file['removed'] = True + file["byte_size"] = 200 + file["checksum"]["value"] = "changed" + file["parent_directory"] = 1 + file["file_frozen"] = "3" + file["file_frozen"][1:] + file["file_format"] = "changed" + file["file_name"] = "changed" + file["file_path"] = "/oh/no" + file["file_storage"] = 2 + file["file_uploaded"] = "3" + file["file_uploaded"][1:] + file["identifier"] = "changed" + file["open_access"] = True + file["project_identifier"] = "changed" + file["service_modified"] = "changed" + file["service_created"] = "changed" + file["removed"] = True # the only field that should be changed - file['file_characteristics'] = { 'title': 'new title'} + file["file_characteristics"] = {"title": "new title"} - response = self.client.put('/rest/files/1', file, format="json") + response = self.client.put("/rest/files/1", file, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['file_characteristics']['title'], 'new title', response.data) + self.assertEqual(response.data["file_characteristics"]["title"], "new title", response.data) for key, value in response.data.items(): try: - if key in ('date_modified', 'file_modified'): + if key in ("date_modified", "file_modified"): # these fields are changed by metax continue - elif key == 'file_characteristics': + elif key == "file_characteristics": # the field that should have been changed by the user self.assertNotEqual(original_file[key], response.data[key]) else: # must not have changed self.assertEqual(original_file[key], response.data[key]) except KeyError as e: - if e.args[0] == 'user_modified': + if e.args[0] == "user_modified": # added by metax continue raise @responses.activate def test_user_can_update_files_in_their_projects(self): - ''' + """ Ensure users can edit files in projects they are a member of. - ''' - proj = File.objects.only('project_identifier').get(pk=1).project_identifier + """ + proj = File.objects.only("project_identifier").get(pk=1).project_identifier - response = self.client.get('/rest/files?project_identifier=%s' % proj, - format="json") + response = self.client.get("/rest/files?project_identifier=%s" % proj, format="json") - file = response.data['results'][0] + file = response.data["results"][0] - self.token['group_names'].append('IDA01:%s' % proj) - self._use_http_authorization(method='bearer', token=self.token) + self.token["group_names"].append("IDA01:%s" % proj) + self._use_http_authorization(method="bearer", token=self.token) - response = self.client.put('/rest/files/%s' % file['id'], file, format="json") + response = self.client.put("/rest/files/%s" % file["id"], file, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.put('/rest/files', [file], format="json") + response = self.client.put("/rest/files", [file], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) @responses.activate def test_user_cant_update_files_in_others_projects(self): - ''' + """ Ensure users can not edit files in projects they are not a member of. - ''' - proj = File.objects.only('project_identifier').get(pk=1).project_identifier + """ + proj = File.objects.only("project_identifier").get(pk=1).project_identifier - response = self.client.get('/rest/files?project_identifier=%s' % proj, - format="json") + response = self.client.get("/rest/files?project_identifier=%s" % proj, format="json") - file = response.data['results'][0] + file = response.data["results"][0] - self.token['group_names'] = ['no_files_for_this_project'] - self._use_http_authorization(method='bearer', token=self.token) + self.token["group_names"] = ["no_files_for_this_project"] + self._use_http_authorization(method="bearer", token=self.token) - response = self.client.put('/rest/files/%s' % file['id'], file, format="json") + response = self.client.put("/rest/files/%s" % file["id"], file, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.put('/rest/files', [file], format="json") + response = self.client.put("/rest/files", [file], format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -1398,8 +1657,14 @@ def test_dryrun(self): Ensure query parameter ?dryrun=true returns same result as they normally would, but changes made during the request do not get saved in the db. """ - response = self.client.post('/rest/files?what&dryrun=true&other', self.test_new_data, format="json") + response = self.client.post( + "/rest/files?what&dryrun=true&other", self.test_new_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('id' in response.data, True) - found = File.objects.filter(pk=response.data['id']).exists() - self.assertEqual(found, False, 'file should not get truly created when using parameter dryrun') + self.assertEqual("id" in response.data, True) + found = File.objects.filter(pk=response.data["id"]).exists() + self.assertEqual( + found, + False, + "file should not get truly created when using parameter dryrun", + ) diff --git a/src/metax_api/tests/api/rest/base/views/filestorages/read.py b/src/metax_api/tests/api/rest/base/views/filestorages/read.py index 1833c58a..aad76fe9 100755 --- a/src/metax_api/tests/api/rest/base/views/filestorages/read.py +++ b/src/metax_api/tests/api/rest/base/views/filestorages/read.py @@ -14,13 +14,12 @@ class FileStorageApiReadBasicTests(APITestCase, TestClassUtils): - @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(FileStorageApiReadBasicTests, cls).setUpClass() def setUp(self): @@ -28,13 +27,17 @@ def setUp(self): def test_basic_get(self): fs = FileStorage.objects.get(pk=1) - response = self.client.get('/rest/filestorages/%d' % fs.id) + response = self.client.get("/rest/filestorages/%d" % fs.id) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/filestorages/%s' % fs.file_storage_json['identifier']) + response = self.client.get("/rest/filestorages/%s" % fs.file_storage_json["identifier"]) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) def test_basic_list(self): - response = self.client.get('/rest/filestorages') + response = self.client.get("/rest/filestorages") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['results']), FileStorage.objects.all().count(), response.data) + self.assertEqual( + len(response.data["results"]), + FileStorage.objects.all().count(), + response.data, + ) diff --git a/src/metax_api/tests/api/rest/base/views/filestorages/write.py b/src/metax_api/tests/api/rest/base/views/filestorages/write.py index df7183fb..2bdc6f0b 100755 --- a/src/metax_api/tests/api/rest/base/views/filestorages/write.py +++ b/src/metax_api/tests/api/rest/base/views/filestorages/write.py @@ -14,39 +14,40 @@ class FileStorageApiWriteCommon(APITestCase, TestClassUtils): - @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(FileStorageApiWriteCommon, cls).setUpClass() def setUp(self): - self.new_test_data = self._get_object_from_test_data('filestorage') - self.new_test_data.pop('id') - self.new_test_data['file_storage_json']['identifier'] = 'new-file-storage' + self.new_test_data = self._get_object_from_test_data("filestorage") + self.new_test_data.pop("id") + self.new_test_data["file_storage_json"]["identifier"] = "new-file-storage" self._use_http_authorization() class FileStorageApiWriteBasicTests(FileStorageApiWriteCommon): - def test_create(self): - response = self.client.post('/rest/filestorages', self.new_test_data, format="json") + response = self.client.post("/rest/filestorages", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_create_identifier_already_exists(self): - response = self.client.post('/rest/filestorages', self.new_test_data, format="json") + response = self.client.post("/rest/filestorages", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.post('/rest/filestorages', self.new_test_data, format="json") + response = self.client.post("/rest/filestorages", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('already exists' in response.data['file_storage_json']['identifier'][0], - True, response.data) + self.assertEqual( + "already exists" in response.data["file_storage_json"]["identifier"][0], + True, + response.data, + ) def test_delete(self): - response = self.client.delete('/rest/filestorages/1') + response = self.client.delete("/rest/filestorages/1") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) fs = FileStorage.objects_unfiltered.get(pk=1) - self.assertEqual(fs.removed, True, 'should be deleted') + self.assertEqual(fs.removed, True, "should be deleted") self.assertEqual(fs.date_removed, fs.date_modified) diff --git a/src/metax_api/tests/api/rest/base/views/schemas/read.py b/src/metax_api/tests/api/rest/base/views/schemas/read.py index eea13fd6..88122020 100755 --- a/src/metax_api/tests/api/rest/base/views/schemas/read.py +++ b/src/metax_api/tests/api/rest/base/views/schemas/read.py @@ -12,25 +12,24 @@ class SchemaApiReadTests(APITestCase, TestClassUtils): - def test_read_schemas_list(self): - response = self.client.get('/rest/schemas') + response = self.client.get("/rest/schemas") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['count'] > 0) + self.assertTrue(response.data["count"] > 0) def test_read_schemas_list_html(self): - headers = {'HTTP_ACCEPT': 'text/html'} - response = self.client.get('/rest/schemas', **headers) + headers = {"HTTP_ACCEPT": "text/html"} + response = self.client.get("/rest/schemas", **headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response._headers['content-type'][1].find('text/html') >= 0) + self.assertTrue(response._headers["content-type"][1].find("json") >= 0) def test_read_schema_retrieve_existing(self): - list_response = self.client.get('/rest/schemas') + list_response = self.client.get("/rest/schemas") self.assertEqual(list_response.status_code, status.HTTP_200_OK) - self.assertTrue(list_response.data['count'] > 0, 'No schemas available') - response = self.client.get('/rest/schemas/%s' % list_response.data['results'][0]) + self.assertTrue(list_response.data["count"] > 0, "No schemas available") + response = self.client.get("/rest/schemas/%s" % list_response.data["results"][0]) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_read_schema_not_exists(self): - response = self.client.get('/rest/schemas/thisshouldnotexist') + response = self.client.get("/rest/schemas/thisshouldnotexist") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/src/metax_api/tests/api/rest/v2/views/apierrors/read.py b/src/metax_api/tests/api/rest/v2/views/apierrors/read.py index 4610453b..ce8965e7 100755 --- a/src/metax_api/tests/api/rest/v2/views/apierrors/read.py +++ b/src/metax_api/tests/api/rest/v2/views/apierrors/read.py @@ -4,16 +4,18 @@ # # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT +import logging +from unittest.mock import patch +from uuid import uuid4 -from os import makedirs -from shutil import rmtree - -from django.conf import settings from django.core.management import call_command from rest_framework import status from rest_framework.test import APITestCase -from metax_api.tests.utils import TestClassUtils, test_data_file_path +from metax_api.models import ApiError +from metax_api.tests.utils import TestClassUtils, test_data_file_path, testcase_log_console + +_logger = logging.getLogger(__name__) class ApiErrorReadBasicTests(APITestCase, TestClassUtils): @@ -27,128 +29,171 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(ApiErrorReadBasicTests, cls).setUpClass() def setUp(self): super(ApiErrorReadBasicTests, self).setUp() - rmtree(settings.ERROR_FILES_PATH, ignore_errors=True) - makedirs(settings.ERROR_FILES_PATH) self._use_http_authorization(username="metax") - def _assert_fields_presence(self, response): + def mock_api_error_consume(self): + """ + ApiErrors are created when fetching Rabbitmq queue but when running testcases Rabbitmq is not available. + This mocks the consume part while the publishing part is actually not doing anything. + """ + error = { + "method": "POST", + "user": "metax", + "data": { "metadata_owner_org": "abc-org-123" }, + "headers": { + "HTTP_COOKIE": "" + }, + "status_code": 400, + "response": { + "data_catalog": [ + "ErrorDetail(string=This field is required., code=required)" + ] + }, + "traceback": + "Traceback(most recent call last): File/usr/local/lib/python3.8/site-packages/rest_framework/views.py", + "url": "/rest/datasets", + "identifier": f"2021-06-29T11:10:54-{str(uuid4())[:8]}", + "exception_time": "2021-06-29T11:10:54+00:00", + "other": { + "bulk_request": True, + "data_row_count": 2 + } + } + ApiError.objects.create(identifier=error["identifier"], error=error) + + def _assert_fields_presence(self, error_json): """ Check presence and absence of some key information. """ - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('data' in response.data, True, response.data) - self.assertEqual('response' in response.data, True, response.data) - self.assertEqual('traceback' in response.data, True, response.data) - self.assertEqual('url' in response.data, True, response.data) - self.assertEqual('HTTP_AUTHORIZATION' in response.data['headers'], False, response.data['headers']) - + self.assertEqual("identifier" in error_json, True, error_json) + self.assertEqual("data" in error_json, True, error_json) + self.assertEqual("response" in error_json, True, error_json) + self.assertEqual("traceback" in error_json, True, error_json) + self.assertEqual("url" in error_json, True, error_json) + self.assertEqual( + "HTTP_AUTHORIZATION" in error_json["headers"], + False, + error_json["headers"], + ) + + @patch("metax_api.services.rabbitmq_service._RabbitMQServiceDummy.consume_api_errors", mock_api_error_consume) def test_list_errors(self): """ Each requesting resulting in an error should leave behind one API error entry. """ - cr_1 = self.client.get('/rest/v2/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1.pop('data_catalog') # causes an error + cr_1 = self.client.get("/rest/v2/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1.pop("data_catalog") # causes an error - response = self.client.post('/rest/v2/datasets', cr_1, format='json') + response = self.client.post("/rest/v2/datasets", cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.post('/rest/v2/datasets', cr_1, format='json') + response = self.client.post("/rest/v2/datasets", cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/v2/apierrors') + response = self.client.get("/rest/v2/apierrors") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + @patch("metax_api.services.rabbitmq_service._RabbitMQServiceDummy.consume_api_errors", mock_api_error_consume) def test_get_error_details(self): - cr_1 = self.client.get('/rest/v2/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1.pop('data_catalog') # causes an error - cr_1['research_dataset']['title'] = { 'en': 'Abc' } + self.apierror_identifier = f"2021-06-29T11:10:54-{str(uuid4())[:8]}" + + cr_1 = self.client.get("/rest/v2/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1.pop("data_catalog") # causes an error + cr_1["research_dataset"]["title"] = {"en": "Abc"} - response = self.client.post('/rest/v2/datasets', cr_1, format='json') + response = self.client.post("/rest/v2/datasets", cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - # list errors in order to get error identifier - response = self.client.get('/rest/v2/apierrors') + error = ApiError.objects.last() + + response = self.client.get(f"/rest/v2/apierrors/{error.identifier}") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('identifier' in response.data[0], True, response.data) - response = self.client.get('/rest/v2/apierrors/%s' % response.data[0]['identifier']) - self._assert_fields_presence(response) - self.assertEqual('data_catalog' in response.data['response'], True, response.data['response']) - self.assertEqual(response.data['data']['research_dataset']['title']['en'], 'Abc', - response.data['data']['research_dataset']['title']) + error_json = response.data["error"] + + self._assert_fields_presence(error_json) + self.assertTrue("data_catalog" in error_json["response"], error_json["response"]) + @patch("metax_api.services.rabbitmq_service._RabbitMQServiceDummy.consume_api_errors", mock_api_error_consume) + @testcase_log_console(_logger) def test_delete_error_details(self): - cr_1 = self.client.get('/rest/v2/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1.pop('data_catalog') # causes an error + cr_1 = self.client.get("/rest/v2/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1.pop("data_catalog") # causes an error - response = self.client.post('/rest/v2/datasets', cr_1, format='json') + response = self.client.post("/rest/v2/datasets", cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/v2/apierrors') - response = self.client.delete('/rest/v2/apierrors/%s' % response.data[0]['identifier']) + error = ApiError.objects.last() + + response = self.client.delete(f"/rest/v2/apierrors/{error.identifier}") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - response = self.client.get('/rest/v2/apierrors') + response = self.client.get("/rest/v2/apierrors") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + @patch("metax_api.services.rabbitmq_service._RabbitMQServiceDummy.consume_api_errors", mock_api_error_consume) + @testcase_log_console(_logger) def test_delete_all_error_details(self): - cr_1 = self.client.get('/rest/v2/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1.pop('data_catalog') # causes an error + cr_1 = self.client.get("/rest/v2/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1.pop("data_catalog") # causes an error - response = self.client.post('/rest/v2/datasets', cr_1, format='json') + response = self.client.post("/rest/v2/datasets", cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - response = self.client.post('/rest/v2/datasets', cr_1, format='json') + response = self.client.post("/rest/v2/datasets", cr_1, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # ensure something was produced... - response = self.client.get('/rest/v2/apierrors') + response = self.client.get("/rest/v2/apierrors") - response = self.client.post('/rest/v2/apierrors/flush') + response = self.client.post("/rest/v2/apierrors/flush") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/v2/apierrors') + response = self.client.get("/rest/v2/apierrors") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + @patch("metax_api.services.rabbitmq_service._RabbitMQServiceDummy.consume_api_errors", mock_api_error_consume) def test_bulk_operation_produces_error_entry(self): """ Ensure also bulk operations produce error entries. """ - cr_1 = self.client.get('/rest/v2/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1.pop('data_catalog') # causes an error - response = self.client.post('/rest/v2/datasets', [cr_1, cr_1], format='json') + cr_1 = self.client.get("/rest/v2/datasets/1").data + cr_1.pop("id") + cr_1.pop("identifier") + cr_1.pop("data_catalog") # causes an error + response = self.client.post("/rest/v2/datasets", [cr_1, cr_1], format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/v2/apierrors') + response = self.client.get("/rest/v2/apierrors") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/v2/apierrors/%s' % response.data[0]['identifier']) - self._assert_fields_presence(response) - self.assertEqual('other' in response.data, True, response.data) - self.assertEqual('bulk_request' in response.data['other'], True, response.data) - self.assertEqual('data_row_count' in response.data['other'], True, response.data) + error = ApiError.objects.last() + + self._assert_fields_presence(error.error) + + self.assertEqual("other" in error.error, True, response.data) + self.assertEqual("bulk_request" in error.error["other"], True, response.data) + self.assertEqual("data_row_count" in error.error["other"], True, response.data) def test_api_permitted_only_to_metax_user(self): # uses testuser by default self._use_http_authorization() - response = self.client.get('/rest/v2/apierrors') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.get('/rest/v2/apierrors/123') + response = self.client.get("/rest/v2/apierrors") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.delete('/rest/v2/apierrors/123') + response = self.client.get("/rest/v2/apierrors/123") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.post('/rest/v2/apierrors/flush_errors') + response = self.client.delete("/rest/v2/apierrors/123") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + response = self.client.post("/rest/v2/apierrors/flush_errors") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/common/__init__.py b/src/metax_api/tests/api/rest/v2/views/common/__init__.py index 74a7ae5e..fb7f7576 100755 --- a/src/metax_api/tests/api/rest/v2/views/common/__init__.py +++ b/src/metax_api/tests/api/rest/v2/views/common/__init__.py @@ -6,5 +6,4 @@ # :license: MIT from .auth import * -from .read import * -from .write import * +from .read import * \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/common/auth.py b/src/metax_api/tests/api/rest/v2/views/common/auth.py index ea5c9730..6b986c66 100755 --- a/src/metax_api/tests/api/rest/v2/views/common/auth.py +++ b/src/metax_api/tests/api/rest/v2/views/common/auth.py @@ -7,14 +7,13 @@ import logging -import responses from rest_framework import status from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon -from metax_api.tests.utils import get_test_oidc_token _logger = logging.getLogger(__name__) + class ApiServiceAccessAuthorization(CatalogRecordApiWriteCommon): """ @@ -24,129 +23,12 @@ class ApiServiceAccessAuthorization(CatalogRecordApiWriteCommon): def setUp(self): super().setUp() # test user api_auth_user has some custom api permissions set in settings.py - self._use_http_authorization(username='api_auth_user') - - def test_read_access_ok(self): - """ - User api_auth_user should have read access to files api. - """ - response = self.client.get('/rest/v2/files/1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_read_access_fail(self): - """ - Anonymous user should not be able to access files and should not have any information - about the existence of requested file. - """ - self.client._credentials = {} - response = self.client.get('/rest/v2/files/1') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_create_access_ok(self): - """ - User api_auth_user should have create access to datasets api. - """ - response = self.client.get('/rest/v2/datasets/1') - cr = response.data - cr['contract'] = 1 - response = self.client.put('/rest/v2/datasets/1', cr, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_update_access_error(self): - """ - User api_auth_user should not have update access to files api. - """ - response = self.client.get('/rest/v2/files/1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - file = response.data - file['file_format'] = 'text/html' - - response = self.client.put('/rest/v2/files/1', file, format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_delete_access_error(self): - """ - User api_auth_user should not have delete access to files api. - """ - response = self.client.delete('/rest/v2/files/1') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self._use_http_authorization(username="api_auth_user") def test_read_for_world_ok(self): """ Reading datasets api should be permitted even without any authorization. """ self.client._credentials = {} - response = self.client.get('/rest/v2/datasets/1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - -class ApiEndUserAccessAuthorization(CatalogRecordApiWriteCommon): - - """ - Test End User authentication and authorization. - - Since the End User authnz utilizes OIDC, and there is no legit local OIDC OP, - responses from /secure/validate_token are mocked. The endpoint only returns - 200 OK for successful token validation, or 403 for failed validation. - """ - - def setUp(self): - super().setUp() - self._use_http_authorization(method='bearer', token=get_test_oidc_token()) - - @responses.activate - def test_valid_token(self): - """ - Test api authentication with a valid token. Validation is mocked, ensures code following - valid authentication works. Should return successfully. - """ - self._mock_token_validation_succeeds() - response = self.client.get('/rest/v2/datasets/1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - @responses.activate - def test_invalid_token(self): - """ - Test api authentication with an invalid token. Validation is mocked, ensures code following - failed authentication works. Should return 403. - - Note: This test basically takes care of a lot of test cases regarding token validation. Since - token validation is executed by apache, it is rather opaque for testing purposes here, returning - only 403 on failures (or possibly 401 on some cases). - - Possible reasons for failures include: - - expired token - - invalid signature - - malformed token - - bad claims (such as intended audience) - - In all cases, metax code execution stops at the middleware where authentication failed. - """ - self._mock_token_validation_fails() - response = self.client.get('/rest/v2/datasets/1') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - @responses.activate - def test_end_user_read_access(self): - """ - Ensure end users are recognized in api read access permissions. - """ - self._mock_token_validation_succeeds() - - # datasets-api should be allowed for end users - response = self.client.get('/rest/v2/datasets/1') + response = self.client.get("/rest/v2/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK) - - # contracts-api should not be allowed for end users - response = self.client.get('/rest/v2/contracts/1') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - @responses.activate - def test_end_user_create_access_error(self): - """ - Ensure end users are recognized in api create access permissions. - """ - self._mock_token_validation_succeeds() - # end users should not have create access to files api. - response = self.client.post('/rest/v2/files', {}, format='json') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) diff --git a/src/metax_api/tests/api/rest/v2/views/common/read.py b/src/metax_api/tests/api/rest/v2/views/common/read.py index f902c97e..757a800a 100755 --- a/src/metax_api/tests/api/rest/v2/views/common/read.py +++ b/src/metax_api/tests/api/rest/v2/views/common/read.py @@ -5,71 +5,11 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from datetime import timedelta - -from django.utils import timezone -from pytz import timezone as tz from rest_framework import status -from metax_api.models import CatalogRecord, File from metax_api.tests.api.rest.base.views.datasets.read import CatalogRecordApiReadCommon -class ApiReadGetDeletedObjects(CatalogRecordApiReadCommon): - - """ - Test use of query parameter removed=bool, which is common for all apis - """ - - def test_removed_query_param(self): - obj = CatalogRecord.objects.get(pk=1) - obj.removed = True - obj.force_save() - obj2 = CatalogRecord.objects.get(pk=2) - obj2.removed = True - obj2.force_save() - response = self.client.get('/rest/v2/datasets/1') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - response = self.client.get('/rest/v2/datasets/1?removed=true') - self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.get('/rest/v2/datasets/metadata_version_identifiers') - self.assertEqual(obj.metadata_version_identifier not in response.data, True) - self.assertEqual(obj2.metadata_version_identifier not in response.data, True) - - obj = File.objects.get(pk=1) - obj.removed = True - obj.force_save() - obj2 = File.objects.get(pk=2) - obj2.removed = True - obj2.force_save() - response = self.client.get('/rest/v2/files/1') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - response = self.client.get('/rest/v2/files/1?removed=true') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_removed_parameter_gets_correct_amount_of_objects(self): - path = '/rest/v2/datasets' - objects = CatalogRecord.objects.all().values() - - results = self.client.get('{0}?pagination=false&removed=false'.format(path)).json() - initial_amt = len(results) - - results = self.client.get('{0}?pagination=false&removed=true'.format(path)).json() - self.assertEqual(len(results), 0, "Without removed objects remove=true should return 0 results") - - self._use_http_authorization() - amt_to_delete = 2 - for i in range(amt_to_delete): - response = self.client.delete('{0}/{1}'.format(path, objects[i]['id'])) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - - results = self.client.get('{0}?pagination=false&removed=false'.format(path)).json() - self.assertEqual(len(results), initial_amt - amt_to_delete, "Non-removed object amount is incorrect") - - results = self.client.get('{0}?pagination=false&removed=true'.format(path)).json() - self.assertEqual(len(results), amt_to_delete, "Removed object amount is incorrect") - - class ApiReadPaginationTests(CatalogRecordApiReadCommon): """ @@ -77,189 +17,16 @@ class ApiReadPaginationTests(CatalogRecordApiReadCommon): """ def test_read_catalog_record_list_pagination_1(self): - for param in ['pagination=true', 'pagination', '']: - response = self.client.get('/rest/datasets?{}&limit=2&offset=0'.format(param)) + for param in ["pagination=true", "pagination", ""]: + response = self.client.get("/rest/datasets?{}&limit=2&offset=0".format(param)) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['results']), 2, 'There should have been exactly two results') - self.assertEqual(response.data['results'][0]['id'], 1, 'Id of first result should have been 1') - - def test_read_catalog_record_list_pagination_2(self): - for param in ['pagination=true', 'pagination', '']: - response = self.client.get('/rest/datasets?{}&limit=2&offset=2'.format(param)) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['results']), 2, 'There should have been exactly two results') - self.assertEqual(response.data['results'][0]['id'], 3, 'Id of first result should have been 3') - - def test_disable_pagination(self): - response = self.client.get('/rest/datasets?pagination=false') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('next' not in response.data, True) - self.assertEqual('results' not in response.data, True) - - def test_pagination_ordering(self): - limit = 5 - - for order in ('preservation_state', '-preservation_state'): - - # vary offset from 0 to 20, in increments of 5 - for offset in range(0, 20, 5): - - response = self.client.get(f'/rest/v2/datasets?limit={limit}&offset={offset}&ordering={order}') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - from_api = [cr['preservation_state'] for cr in response.data['results']] - - from_db = [ - r for r in CatalogRecord.objects - .filter() - .order_by(order) - .values_list('preservation_state', flat=True)[offset:offset + limit] - ] - - self.assertEqual(from_api, from_db) - - -class ApiReadHTTPHeaderTests(CatalogRecordApiReadCommon): - # - # header if-modified-since tests, single - # - - # If the value of the timestamp given in the header is equal or greater than the value of date_modified field, - # 404 should be returned since nothing has been modified. If the value of the timestamp given in the header is - # less than value of date_modified field, the object should be returned since it means the object has been - # modified after the header timestamp - - def test_get_with_if_modified_since_header_ok(self): - cr = CatalogRecord.objects.get(pk=self.pk) - date_modified = cr.date_modified - date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz('GMT')) - - if_modified_since_header_value = date_modified_in_gmt.strftime('%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/v2/datasets/%s' % self.identifier, **headers) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - if_modified_since_header_value = (date_modified_in_gmt + timedelta(seconds=1)).strftime( - '%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/v2/datasets/%s' % self.identifier, **headers) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - if_modified_since_header_value = (date_modified_in_gmt - timedelta(seconds=1)).strftime( - '%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/v2/datasets/%s' % self.identifier, **headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_get_with_if_modified_since_header_syntax_error(self): - cr = CatalogRecord.objects.get(pk=self.pk) - date_modified = cr.date_modified - date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz('GMT')) - - if_modified_since_header_value = date_modified_in_gmt.strftime('%a, %d %b %Y %H:%M:%S UTC') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/v2/datasets/%s' % self.identifier, **headers) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # - # header if-modified-since tests, list - # - - # List operation returns always 200 even if no datasets match the if-modified-since criterium - - def test_list_get_with_if_modified_since_header_ok(self): - cr = CatalogRecord.objects.get(pk=self.pk) - date_modified = cr.date_modified - date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz('GMT')) - - if_modified_since_header_value = date_modified_in_gmt.strftime('%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/v2/datasets?limit=100', **headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(len(response.data.get('results')) == 6) - - if_modified_since_header_value = (date_modified_in_gmt + timedelta(seconds=1)).strftime( - '%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/v2/datasets?limit=100', **headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(len(response.data.get('results')) == 6) - - # The asserts below may brake if the date_modified timestamps or the amount of test data objects are altered - # in the test data - - if_modified_since_header_value = (date_modified_in_gmt - timedelta(seconds=1)).strftime( - '%a, %d %b %Y %H:%M:%S GMT') - headers = {'HTTP_IF_MODIFIED_SINCE': if_modified_since_header_value} - response = self.client.get('/rest/v2/datasets?limit=100', **headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(len(response.data.get('results')) > 6) - self.assertTrue(len(response.data.get('results')) == 28) - - # should also work with records that have been recently created, and date_modified is empty - - cr.date_created = date_modified - cr.date_modified = None - cr.force_save() - response = self.client.get('/rest/v2/datasets?limit=100', **headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(len(response.data.get('results')) > 6) - self.assertTrue(len(response.data.get('results')) == 28) - - -class ApiReadQueryParamTests(CatalogRecordApiReadCommon): - - """ - Misc common query params tests - """ - - def test_return_requested_fields_only(self): - """ - While the param ?fields works with write operations too, the primary use case is when GETting. - """ - response = self.client.get('/rest/v2/datasets?fields=identifier') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('identifier' in response.data['results'][0], True) - self.assertEqual(len(response.data['results'][0].keys()), 1) - self.assertEqual(len(response.data['results'][1].keys()), 1) - - response = self.client.get('/rest/v2/datasets/1?fields=identifier') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('identifier' in response.data, True) - self.assertEqual(len(response.data.keys()), 1) - - response = self.client.get('/rest/v2/datasets/1?fields=identifier,data_catalog') - self.assertEqual('identifier' in response.data, True) - self.assertEqual('data_catalog' in response.data, True) - self.assertEqual(len(response.data.keys()), 2) - - response = self.client.get('/rest/v2/datasets/1?fields=not_found') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # Anonymous user using fields parameter and not including research_dataset should not cause crashing - self.client._credentials = {} - response = self.client.get('/rest/v2/datasets/1?fields=identifier') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_checksum_field_for_file(self): - """ - Check that checksum field works correctly - """ - - self._use_http_authorization('metax') - response = self.client.get('/rest/v2/files/1?fields=checksum') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data.get('checksum'), 'Checksum JSON should be returned') - self.assertTrue(response.data['checksum'].get('algorithm')) - self.assertTrue(response.data['checksum'].get('checked')) - self.assertTrue(response.data['checksum'].get('value')) - - response = self.client.get('/rest/v2/files/1?fields=checksum:value') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data.get('checksum'), 'Checksum JSON should be returned') - self.assertTrue(response.data['checksum'].get('value')) - self.assertFalse(response.data['checksum'].get('algorithm')) - - response = self.client.get('/rest/v2/files/1?fields=checksum:badvalue') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('is not part of' in response.data['detail'][0], 'Should complain about field not found') + self.assertEqual( + len(response.data["results"]), + 2, + "There should have been exactly two results", + ) + self.assertEqual( + response.data["results"][0]["id"], + 1, + "Id of first result should have been 1", + ) \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/common/write.py b/src/metax_api/tests/api/rest/v2/views/common/write.py deleted file mode 100755 index 99cbeb6b..00000000 --- a/src/metax_api/tests/api/rest/v2/views/common/write.py +++ /dev/null @@ -1,301 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from copy import deepcopy -from time import sleep - -from django.core.management import call_command -from rest_framework import status -from rest_framework.test import APITestCase - -from metax_api.models import CatalogRecord -from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon -from metax_api.tests.utils import TestClassUtils, test_data_file_path - -""" -Common phenomenas that concern all API's. - -The particular API selected for a test should not matter. However, -choosing to do tests using api /datasets is not a bad choice, since that -API is currently the most complex, where things are most likely to experience -a RUD. -""" - - -class ApiWriteCommon(APITestCase, TestClassUtils): - - def setUp(self): - call_command('loaddata', test_data_file_path, verbosity=0) - self.test_new_data = self._get_new_test_data() - self._use_http_authorization() - - def _get_new_test_data(self): - record_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=0) - record_from_test_data.update({ - "data_catalog": 1, - }) - record_from_test_data['research_dataset'].update({ - "preferred_identifier": None, - }) - record_from_test_data.pop('id', None) - record_from_test_data.pop('identifier', None) - record_from_test_data.pop('contract', None) - return record_from_test_data - - -class ApiWriteCommonFieldsTests(ApiWriteCommon): - - def test_certain_create_fields_are_read_only_after_create(self): - """ - The following fields should be read-only after initial creation of a resource: - - date_created - - user_created - - service_created - """ - response = self.client.post('/rest/v2/datasets', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - # some of the fields could be empty in test data. that is fine tho, the point is that - # they should not change later. - orig_date_created = response.data.get('date_created', None) - orig_user_created = response.data.get('user_created', None) - orig_service_created = response.data.get('service_created', None) - - altered = response.data - altered['date_created'] = altered['date_created'].replace('2017', '2010') - altered['user_created'] = 'changed' - altered['service_created'] = 'changed' - - response = self.client.put('/rest/v2/datasets/%d' % altered['id'], altered, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - response = self.client.get('/rest/v2/datasets/%d' % altered['id'], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(orig_date_created, response.data.get('date_created', None)) - self.assertEqual(orig_user_created, response.data.get('user_created', None)) - self.assertEqual(orig_service_created, response.data.get('service_created', None)) - - def test_deletion_sets_removed_true_and_sets_value_for_date_removed(self): - response = self.client.post('/rest/v2/datasets', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr_id = response.data['id'] - response = self.client.delete('/rest/v2/datasets/%d' % cr_id) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - - # Verify date_removed got set - response = self.client.get('/rest/v2/datasets/%d?removed' % cr_id) - self.assertTrue(response.data['removed'] is True) - self.assertTrue(response.data.get('date_removed', '').startswith('2')) - - def test_updating_sets_removed_false_and_empties_date_removed(self): - response = self.client.post('/rest/v2/datasets', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr_id = response.data['id'] - response = self.client.delete('/rest/v2/datasets/%d' % cr_id) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - rd = self.client.get('/rest/v2/datasets/%d?removed' % cr_id).data - rd_date_rem = rd['date_removed'] - sleep(1) # ensure that next request happens with different timestamp - response = self.client.put('/rest/v2/datasets/%d?removed' % cr_id, rd, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - response = self.client.get('/rest/v2/datasets/%d' % cr_id) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data['removed'] is False) - self.assertTrue(response.data.get('date_removed') is None) - self.assertTrue(response.data.get('date_modified') != rd_date_rem, 'date_modified should be updated') - - -class ApiWriteHTTPHeaderTests(CatalogRecordApiWriteCommon): - - # - # header if-unmodified-since tests, single - # - - def test_update_with_if_unmodified_since_header_ok(self): - cr = self.client.get('/rest/v2/datasets/1').data - cr['preservation_description'] = 'damn this is good coffee' - - cr_obj = CatalogRecord.objects.get(pk=1) - headers = {'HTTP_IF_UNMODIFIED_SINCE': cr_obj.date_modified.strftime('%a, %d %b %Y %H:%M:%S GMT')} - - response = self.client.put('/rest/v2/datasets/1', cr, format="json", **headers) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_update_with_if_unmodified_since_header_precondition_failed_error(self): - cr = self.client.get('/rest/v2/datasets/1').data - cr['preservation_description'] = 'the owls are not what they seem' - - headers = {'HTTP_IF_UNMODIFIED_SINCE': 'Wed, 23 Sep 2009 22:15:29 GMT'} - - response = self.client.put('/rest/v2/datasets/1', cr, format="json", **headers) - self.assertEqual(response.status_code, 412, 'http status should be 412 = precondition failed') - - def test_update_with_if_unmodified_since_header_syntax_error(self): - cr = self.client.get('/rest/v2/datasets/1').data - cr['preservation_description'] = 'the owls are not what they seem' - - cr_obj = CatalogRecord.objects.get(pk=1) - headers = {'HTTP_IF_UNMODIFIED_SINCE': cr_obj.date_modified.strftime('%a, %d %b %Y %H:%M:%S UTC')} - - response = self.client.put('/rest/v2/datasets/1', cr, format="json", **headers) - self.assertEqual(response.status_code, 400, 'http status should be 400') - - # - # header if-unmodified-since tests, list - # - - def test_update_list_with_if_unmodified_since_header_ok(self): - data_1 = self.client.get('/rest/v2/datasets/1', format="json").data - data_2 = self.client.get('/rest/v2/datasets/2', format="json").data - - data_1['preservation_description'] = 'damn this is good coffee' - data_2['preservation_description'] = 'damn this is good coffee also' - - headers = {'HTTP_IF_UNMODIFIED_SINCE': 'value is not checked'} - response = self.client.put('/rest/v2/datasets', [data_1, data_2], format="json", **headers) - - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_update_list_with_if_unmodified_since_header_error_1(self): - """ - One resource being updated was updated in the meantime, resulting in an error - """ - data_1 = self.client.get('/rest/v2/datasets/1', format="json").data - data_2 = self.client.get('/rest/v2/datasets/2', format="json").data - - data_1['preservation_description'] = 'damn this is good coffee' - - # should result in error for this record - data_2['date_modified'] = '2002-01-01T10:10:10Z' - - headers = {'HTTP_IF_UNMODIFIED_SINCE': 'value is not checked'} - response = self.client.put('/rest/v2/datasets', [data_1, data_2], format="json", **headers) - self.assertEqual(len(response.data['failed']) == 1, True, 'there should be only one failed update') - self.assertEqual('modified' in response.data['failed'][0]['errors']['detail'][0], True, - 'error should indicate resource has been modified') - - def test_update_list_with_if_unmodified_since_header_error_2(self): - """ - Field date_modified is missing, while if-modified-since header is set, resulting in an error. - """ - data_1 = self.client.get('/rest/v2/datasets/1', format="json").data - data_2 = self.client.get('/rest/v2/datasets/2', format="json").data - - data_1['preservation_description'] = 'damn this is good coffee' - - # should result in error for this record - data_2.pop('date_modified') - - headers = {'HTTP_IF_UNMODIFIED_SINCE': 'value is not checked'} - response = self.client.patch('/rest/v2/datasets', [data_1, data_2], format="json", **headers) - self.assertEqual('required' in response.data['failed'][0]['errors']['detail'][0], True, - 'error should be about field date_modified is required') - - def test_update_list_with_if_unmodified_since_header_error_3(self): - """ - One resource being updated has never been modified before. Make sure that date_modified = None - is an accepted value. The end result should be that the resource has been modified, since the - server version has a timestamp set in date_modified. - """ - data_1 = self.client.get('/rest/v2/datasets/1', format="json").data - data_2 = self.client.get('/rest/v2/datasets/2', format="json").data - - data_1['preservation_description'] = 'damn this is good coffee' - data_2['preservation_description'] = 'damn this is good coffee also' - data_2['date_modified'] = None - - headers = {'HTTP_IF_UNMODIFIED_SINCE': 'value is not checked'} - response = self.client.put('/rest/v2/datasets', [data_1, data_2], format="json", **headers) - self.assertEqual('modified' in response.data['failed'][0]['errors']['detail'][0], True, - 'error should indicate resource has been modified') - - -class ApiWriteAtomicBulkOperations(CatalogRecordApiWriteCommon): - - """ - Test use of ?atomic=true/false parameter in bulk create and update operations. When atomic flag - is used, all changes should be rolled back if even one operation fails. - """ - - def test_atomic_create(self): - cr = self.client.get('/rest/v2/datasets/1', format="json").data - cr.pop('id') - cr.pop('identifier') - cr['research_dataset'].pop('metadata_version_identifier') - cr['research_dataset'].pop('preferred_identifier') - cr2 = deepcopy(cr) - cr3 = deepcopy(cr) - cr3.pop('data_catalog') # causes error - - record_count_before = CatalogRecord.objects.all().count() - - response = self.client.post('/rest/v2/datasets?atomic=true', [cr, cr2, cr3], format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.content) - self.assertEqual(len(response.data['success']) == 0, True, response.data) - self.assertEqual(len(response.data['failed']) == 1, True, response.data) - self.assertEqual('detail' in response.data, True, response.data) - self.assertEqual('atomic' in response.data['detail'][0], True, response.data) - self.assertEqual(record_count_before, CatalogRecord.objects.all().count(), 'shouldnt create new records') - - def test_atomic_update(self): - cr = self.client.get('/rest/v2/datasets/1', format="json").data - cr2 = self.client.get('/rest/v2/datasets/2', format="json").data - cr3 = self.client.get('/rest/v2/datasets/3', format="json").data - cr['research_dataset']['title']['en'] = 'updated' - cr2['research_dataset']['title']['en'] = 'updated' - cr3.pop('data_catalog') # causes error - - record_count_before = CatalogRecord.objects.all().count() - - response = self.client.put('/rest/v2/datasets?atomic=true', [cr, cr2, cr3], format="json") - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(len(response.data['success']) == 0, True) - self.assertEqual(len(response.data['failed']) == 1, True) - self.assertEqual('atomic' in response.data['detail'][0], True) - self.assertEqual(record_count_before, CatalogRecord.objects.all().count(), 'shouldnt create new versions') - - cr = self.client.get('/rest/v2/datasets/1', format="json").data - cr2 = self.client.get('/rest/v2/datasets/2', format="json").data - self.assertEqual(cr['research_dataset']['title']['en'] == 'updated', False) - self.assertEqual(cr2['research_dataset']['title']['en'] == 'updated', False) - - -class ApiWriteQueryParamTests(ApiWriteCommon): - - """ - Misc common query params tests - """ - - def test_dryrun(self): - """ - Ensure query parameter ?dryrun=true returns same result as they normally would, but - changes made during the request do not get saved in the db. - """ - response = self.client.post('/rest/v2/datasets?dryrun=true', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('id' in response.data, True) - found = CatalogRecord.objects.filter(pk=response.data['id']).exists() - self.assertEqual(found, False, 'record should not get truly created when using parameter dryrun') - - -class ApiWriteCommonOperations(ApiWriteCommon): - """ - Common write operations - """ - - def test_create_file_with_empty_body_fails(self): - response = self.client.post('/rest/datasets') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('Request body is required' in response.data['detail'][0]) - - response = self.client.post('/rest/files') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('Request body is required' in response.data['detail'][0]) \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/contracts/__init__.py b/src/metax_api/tests/api/rest/v2/views/contracts/__init__.py deleted file mode 100755 index bd6a6a7f..00000000 --- a/src/metax_api/tests/api/rest/v2/views/contracts/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from .contracts import ContractApiReadTestV1, ContractApiWriteTestV1 diff --git a/src/metax_api/tests/api/rest/v2/views/contracts/contracts.py b/src/metax_api/tests/api/rest/v2/views/contracts/contracts.py deleted file mode 100755 index aa2e11ee..00000000 --- a/src/metax_api/tests/api/rest/v2/views/contracts/contracts.py +++ /dev/null @@ -1,256 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from django.core.management import call_command -from rest_framework import status -from rest_framework.test import APITestCase - -from metax_api.models import CatalogRecord, Contract -from metax_api.tests.utils import TestClassUtils, test_data_file_path - - -class ContractApiReadTestV1(APITestCase, TestClassUtils): - @classmethod - def setUpClass(cls): - """ - Loaded only once for test cases inside this class. - """ - call_command('loaddata', test_data_file_path, verbosity=0) - super(ContractApiReadTestV1, cls).setUpClass() - - def setUp(self): - contract_from_test_data = self._get_object_from_test_data('contract', requested_index=0) - self.pk = contract_from_test_data['id'] - self.identifier = contract_from_test_data['contract_json']['identifier'] - self._use_http_authorization() - - def test_read_contract_list(self): - response = self.client.get('/rest/v2/datasets') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_read_contract_details_by_pk(self): - response = self.client.get('/rest/v2/contracts/%s' % self.pk) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_read_contract_details_by_identifier(self): - response = self.client.get('/rest/v2/contracts/%s' % self.identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_read_contract_details_not_found(self): - response = self.client.get('/rest/v2/contracts/shouldnotexist') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - -class ContractApiWriteTestV1(APITestCase, TestClassUtils): - def setUp(self): - """ - Reloaded for every test case - """ - call_command('loaddata', test_data_file_path, verbosity=0) - self._use_http_authorization() - contract_from_test_data = self._get_object_from_test_data('contract') - self.pk = contract_from_test_data['id'] - - """ - New data that is sent to the server for POST, PUT, PATCH requests. Modified - slightly as approriate for different purposes - """ - self.test_new_data = self._get_new_test_data() - self.second_test_new_data = self._get_second_new_test_data() - self._use_http_authorization() - - def test_create_contract_with_existing_identifier(self): - self.test_new_data['pk'] = self.pk - response = self.client.post('/rest/v2/contracts/', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - response = self.client.post('/rest/v2/contracts/', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('already exists' in response.data['contract_json'][0], - 'Error regarding dublicated identifier') - - def test_update_contract(self): - self.test_new_data['pk'] = self.pk - response = self.client.put('/rest/v2/contracts/%s' % self.pk, self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_update_contract_not_found(self): - response = self.client.put('/rest/v2/contracts/doesnotexist', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_add_catalog_record_to_contract(self): - new_catalog_record = self.client.get('/rest/v2/datasets/1', format="json").data - new_catalog_record.pop('id') - new_catalog_record.pop('identifier') - new_catalog_record['research_dataset'].pop('preferred_identifier') - new_catalog_record['contract'] = self.pk - - response = self.client.post('/rest/v2/datasets', new_catalog_record, format="json") - created_catalog_record = response.data - - try: - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - except Exception: - print(response.data) - raise - self.assertEqual('research_dataset' in created_catalog_record.keys(), True) - self.assertEqual(created_catalog_record['contract']['id'], self.pk) - - contract = Contract.objects.get(pk=self.pk) - try: - contract.records.get(pk=response.data['id']) - except CatalogRecord.DoesNotExist: - raise Exception('The added CatalogRecord should appear in the relation contract.records') - - response = self.client.get('/rest/v2/contracts/%d/datasets' % self.pk) - self.assertIn(created_catalog_record['id'], [cr['id'] for cr in response.data], - 'The added CatalogRecord should appear in the results of /contracts/id/datasets') - - def test_delete_contract(self): - url = '/rest/v2/contracts/%s' % self.pk - response = self.client.delete(url) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - deleted_contract = None - - try: - deleted_contract = Contract.objects.get(pk=self.pk) - except Contract.DoesNotExist: - pass - - if deleted_contract: - raise Exception('Deleted Contract should not be retrievable from the default objects table') - - try: - deleted_contract = Contract.objects_unfiltered.get(pk=self.pk) - except Contract.DoesNotExist: - raise Exception('Deleted contract should not be deleted from the db') - - self.assertEqual(deleted_contract.removed, True, 'Deleted contract should be marked removed in the db') - self.assertEqual(deleted_contract.date_modified, deleted_contract.date_removed, - 'date_modified should be updated') - - def test_delete_contract_catalog_records_are_marked_removed(self): - # add two new records to contract - new_catalog_record = self._get_new_catalog_record_test_data() - new_catalog_record['contract'] = self.pk - self.client.post('/rest/v2/datasets', new_catalog_record, format="json") - self.client.post('/rest/v2/datasets', new_catalog_record, format="json") - - self.client.delete('/rest/v2/contracts/%s' % self.pk) - contract = Contract.objects_unfiltered.get(pk=self.pk) - related_crs = contract.records(manager='objects_unfiltered').all() - response_get_1 = self.client.get('/rest/v2/datasets/%d' % related_crs[0].id) - self.assertEqual(response_get_1.status_code, status.HTTP_404_NOT_FOUND, - 'CatalogRecords of deleted contracts should not be retrievable through the api') - response_get_2 = self.client.get('/rest/v2/datasets/%d' % related_crs[1].id) - self.assertEqual(response_get_2.status_code, status.HTTP_404_NOT_FOUND, - 'CatalogRecords of deleted contracts should not be retrievable through the api') - - for cr in related_crs: - self.assertEqual(cr.removed, True, 'Related CatalogRecord objects should be marked as removed') - - def test_deleted_catalog_record_is_not_listed_in_contract_datasets_api(self): - deleted_id = 1 - self.client.delete('/rest/v2/datasets/%d' % deleted_id) - response = self.client.get('/rest/v2/contracts/%d/datasets' % self.pk) - self.assertNotIn(deleted_id, [cr['id'] for cr in response.data], - 'The deleted CatalogRecord should not appear in the results of /contracts/id/datasets') - - def _get_new_test_data(self): - return { - "contract_json": { - "title": "Title of new contract", - "identifier": "optional-identifier-new", - "quota": 111204, - "created": "2014-01-17T08:19:58Z", - "modified": "2014-01-17T08:19:58Z", - "description": "Description of unknown length", - "contact": [{ - "name": "Contact Name", - "phone": "+358501231234", - "email": "contact.email@csc.fi" - }], - "organization": { - "organization_identifier": "1234567abc", - "name": "Mysterious organization" - }, - "related_service": [{ - "identifier": "local:service:id", - "name": "Name of Service" - }], - "validity": { - "start_date": "2014-01-17" - } - } - } - - def _get_second_new_test_data(self): - return { - "contract_json": { - "title": "Title of second contract", - "identifier": "optional-identifier-for-second", - "quota": 111204, - "created": "2014-01-17T08:19:58Z", - "modified": "2014-01-17T08:19:58Z", - "description": "Description of unknown length", - "contact": [{ - "name": "Contact Name", - "phone": "+358501231234", - "email": "contact.email@csc.fi" - }], - "organization": { - "organization_identifier": "1234567abc", - "name": "Mysterious organization" - }, - "related_service": [{ - "identifier": "local:service:id", - "name": "Name of Service" - }], - "validity": { - "start_date": "2014-01-17" - } - } - } - - def _get_new_catalog_record_test_data(self): - catalog_record_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=0) - return { - "identifier": "http://urn.fi/urn:nbn:fi:iiidentifier", - "data_catalog": self._get_object_from_test_data('datacatalog', requested_index=0), - "research_dataset": { - "modified": "2014-01-17T08:19:58Z", - "version_notes": [ - "This version contains changes to x and y." - ], - "title": { - "en": "Wonderful Title" - }, - "description": [{ - "en": "A descriptive description describing the contents of this dataset. Must be descriptive." - }], - "creator": [{ - "@type": "Person", - "name": "Teppo Testaaja", - "member_of": { - "@type": "Organization", - "name": {"fi": "Mysterious Organization"} - } - }], - "curator": [{ - "@type": "Organization", - "name": {"en": "Curator org", "fi": "Organisaatio"} - }], - "language": [{ - "identifier": "http://lexvo.org/id/iso639-3/aar" - }], - "total_files_byte_size": 1024, - "files": catalog_record_from_test_data['research_dataset']['files'] - } - } diff --git a/src/metax_api/tests/api/rest/v2/views/datacatalogs/__init__.py b/src/metax_api/tests/api/rest/v2/views/datacatalogs/__init__.py index 6adb6e69..d60060fb 100755 --- a/src/metax_api/tests/api/rest/v2/views/datacatalogs/__init__.py +++ b/src/metax_api/tests/api/rest/v2/views/datacatalogs/__init__.py @@ -5,5 +5,4 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from .read import * from .write import * diff --git a/src/metax_api/tests/api/rest/v2/views/datacatalogs/read.py b/src/metax_api/tests/api/rest/v2/views/datacatalogs/read.py deleted file mode 100755 index 22dcc062..00000000 --- a/src/metax_api/tests/api/rest/v2/views/datacatalogs/read.py +++ /dev/null @@ -1,32 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from django.core.management import call_command -from rest_framework import status -from rest_framework.test import APITestCase - -from metax_api.tests.utils import TestClassUtils, test_data_file_path - - -class DataCatalogApiReadBasicTests(APITestCase, TestClassUtils): - @classmethod - def setUpClass(cls): - """ - Loaded only once for test cases inside this class. - """ - call_command('loaddata', test_data_file_path, verbosity=0) - super(DataCatalogApiReadBasicTests, cls).setUpClass() - - def setUp(self): - data_catalog_from_test_data = self._get_object_from_test_data('datacatalog', requested_index=0) - self._use_http_authorization() - self.pk = data_catalog_from_test_data['id'] - self.identifier = data_catalog_from_test_data['catalog_json']['identifier'] - - def test_basic_get(self): - response = self.client.get('/rest/v2/datacatalogs/%s' % self.identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/metax_api/tests/api/rest/v2/views/datacatalogs/write.py b/src/metax_api/tests/api/rest/v2/views/datacatalogs/write.py index 66205590..c0f1dc99 100755 --- a/src/metax_api/tests/api/rest/v2/views/datacatalogs/write.py +++ b/src/metax_api/tests/api/rest/v2/views/datacatalogs/write.py @@ -9,8 +9,6 @@ from rest_framework import status from rest_framework.test import APITestCase -from metax_api.models import DataCatalog -from metax_api.services.redis_cache_service import RedisClient from metax_api.tests.utils import TestClassUtils, test_data_file_path @@ -20,148 +18,31 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(DataCatalogApiWriteCommon, cls).setUpClass() def setUp(self): - self.new_test_data = self._get_object_from_test_data('datacatalog') - self.new_test_data.pop('id') - self.new_test_data['catalog_json']['identifier'] = 'new-data-catalog' + self.new_test_data = self._get_object_from_test_data("datacatalog") + self.new_test_data.pop("id") + self.new_test_data["catalog_json"]["identifier"] = "new-data-catalog" self._use_http_authorization() class DataCatalogApiWriteBasicTests(DataCatalogApiWriteCommon): - def test_identifier_is_auto_generated(self): - response = self.client.post('/rest/v2/datacatalogs', self.new_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertNotEqual(response.data['catalog_json'].get('identifier', None), None, 'identifier should be created') - def test_research_dataset_schema_missing_ok(self): - self.new_test_data['catalog_json'].pop('research_dataset_schema', None) - response = self.client.post('/rest/v2/datacatalogs', self.new_test_data, format="json") + self.new_test_data["catalog_json"].pop("research_dataset_schema", None) + response = self.client.post("/rest/v2/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_research_dataset_schema_not_found_error(self): - self.new_test_data['catalog_json']['research_dataset_schema'] = 'notfound' - response = self.client.post('/rest/v2/datacatalogs', self.new_test_data, format="json") + self.new_test_data["catalog_json"]["research_dataset_schema"] = "notfound" + response = self.client.post("/rest/v2/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_disallow_versioning_in_harvested_catalogs(self): - self.new_test_data['catalog_json']['dataset_versioning'] = True - self.new_test_data['catalog_json']['harvested'] = True - response = self.client.post('/rest/v2/datacatalogs', self.new_test_data, format="json") + self.new_test_data["catalog_json"]["dataset_versioning"] = True + self.new_test_data["catalog_json"]["harvested"] = True + response = self.client.post("/rest/v2/datacatalogs", self.new_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('versioning' in response.data['detail'][0], True, response.data) - - def test_create_identifier_already_exists(self): - response = self.client.post('/rest/v2/datacatalogs', self.new_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.post('/rest/v2/datacatalogs', self.new_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('already exists' in response.data['catalog_json']['identifier'][0], - True, response.data) - - def test_delete(self): - response = self.client.delete('/rest/v2/datacatalogs/1') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - dc_deleted = DataCatalog.objects_unfiltered.get(pk=1) - self.assertEqual(dc_deleted.removed, True) - self.assertEqual(dc_deleted.date_modified, dc_deleted.date_removed, 'date_modified should be updated') - - -class DataCatalogApiWriteReferenceDataTests(DataCatalogApiWriteCommon): - """ - Tests related to reference_data validation and data catalog fields population - from reference_data, according to given uri or code as the value. - """ - - def test_create_data_catalog_with_invalid_reference_data(self): - dc = self.new_test_data['catalog_json'] - dc['field_of_science'][0]['identifier'] = 'nonexisting' - dc['language'][0]['identifier'] = 'nonexisting' - dc['access_rights']['access_type'][0]['identifier'] = 'nonexisting' - dc['access_rights']['license'][0]['identifier'] = 'nonexisting' - response = self.client.post('/rest/v2/datacatalogs', self.new_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('catalog_json' in response.data.keys(), True) - self.assertEqual(len(response.data['catalog_json']), 4) - - def test_create_data_catalog_populate_fields_from_reference_data(self): - """ - 1) Insert codes from cached reference data to data catalog identifier fields - that will be validated, and then populated - 2) Check that that the values in data catalog identifier fields are changed from - codes to uris after a successful create - 3) Check that labels have also been copied to data catalog to their approriate fields - """ - cache = RedisClient() - refdata = cache.get('reference_data')['reference_data'] - orgdata = cache.get('reference_data')['organization_data'] - refs = {} - - data_types = [ - 'access_type', - 'field_of_science', - 'language', - 'license', - ] - - # the values in these selected entries will be used throghout the rest of the test case - for dtype in data_types: - entry = refdata[dtype][0] - refs[dtype] = { - 'code': entry['code'], - 'uri': entry['uri'], - 'label': entry.get('label', None), - } - - refs['organization'] = { - 'uri': orgdata['organization'][1]['uri'], - 'code': orgdata['organization'][1]['code'], - 'label': orgdata['organization'][1]['label'], - } - - # replace the relations with objects that have only the identifier set with code as value, - # to easily check that label was populated (= that it appeared in the dataset after create) - # without knowing its original value from the generated test data - dc = self.new_test_data['catalog_json'] - dc['field_of_science'][0] = {'identifier': refs['field_of_science']['code']} - dc['language'][0] = {'identifier': refs['language']['code']} - dc['access_rights']['access_type'][0] = {'identifier': refs['access_type']['code']} - dc['access_rights']['license'][0] = {'identifier': refs['license']['code']} - - # these have other required fields, so only update the identifier with code - dc['publisher']['identifier'] = refs['organization']['code'] - dc['access_rights']['has_rights_related_agent'][0]['identifier'] = refs['organization']['code'] - - response = self.client.post('/rest/v2/datacatalogs', self.new_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('catalog_json' in response.data.keys(), True) - - new_dc = response.data['catalog_json'] - self._assert_uri_copied_to_identifier(refs, new_dc) - self._assert_label_copied_to_pref_label(refs, new_dc) - self._assert_label_copied_to_title(refs, new_dc) - self._assert_label_copied_to_name(refs, new_dc) - - def _assert_uri_copied_to_identifier(self, refs, new_dc): - self.assertEqual(refs['field_of_science']['uri'], new_dc['field_of_science'][0]['identifier']) - self.assertEqual(refs['language']['uri'], new_dc['language'][0]['identifier']) - self.assertEqual(refs['access_type']['uri'], new_dc['access_rights']['access_type'][0]['identifier']) - self.assertEqual(refs['license']['uri'], new_dc['access_rights']['license'][0]['identifier']) - self.assertEqual(refs['organization']['uri'], new_dc['publisher']['identifier']) - self.assertEqual(refs['organization']['uri'], - new_dc['access_rights']['has_rights_related_agent'][0]['identifier']) - - def _assert_label_copied_to_pref_label(self, refs, new_dc): - self.assertEqual(refs['field_of_science']['label'], new_dc['field_of_science'][0].get('pref_label', None)) - self.assertEqual(refs['access_type']['label'], - new_dc['access_rights']['access_type'][0].get('pref_label', None)) - - def _assert_label_copied_to_title(self, refs, new_dc): - self.assertEqual(refs['license']['label'], new_dc['access_rights']['license'][0].get('title', None)) - - def _assert_label_copied_to_name(self, refs, new_dc): - self.assertEqual(refs['organization']['label'], new_dc['publisher']['name']) + self.assertEqual("versioning" in response.data["detail"][0], True, response.data) \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/__init__.py b/src/metax_api/tests/api/rest/v2/views/datasets/__init__.py index 548b55da..95739e3d 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/__init__.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/__init__.py @@ -10,6 +10,4 @@ from .filehandling import * from .pas import * from .read import * -from .referencedata import * -from .rems import * from .write import * diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/api_version_lock.py b/src/metax_api/tests/api/rest/v2/views/datasets/api_version_lock.py index cf35762e..bd2ec406 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/api_version_lock.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/api_version_lock.py @@ -15,6 +15,7 @@ CR = CatalogRecordV2 + class CatalogRecordApiLock(CatalogRecordApiWriteCommon): """ Tests that no datasets created or edited by v2 api can be further @@ -31,19 +32,19 @@ def _create_v1_dataset(self, cumulative=False, cr=None, draft=False): if cumulative: # not to mess up with the original test dataset cr = deepcopy(cr) - cr['cumulative_state'] = 1 + cr["cumulative_state"] = 1 if draft: - params = 'draft' + params = "draft" else: - params = '' + params = "" - response = self.client.post(f'/rest/datasets?{params}', cr, format='json') + response = self.client.post(f"/rest/datasets?{params}", cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data['api_meta']['version'], 1) + self.assertEqual(response.data["api_meta"]["version"], 1) - cr = CatalogRecord.objects.get(pk=response.data['id']) - self.assertEqual(cr.api_meta['version'], 1, 'api_version should be 1') + cr = CatalogRecord.objects.get(pk=response.data["id"]) + self.assertEqual(cr.api_meta["version"], 1, "api_version should be 1") return response.data @@ -54,26 +55,30 @@ def _create_v2_dataset(self, cumulative=False, cr=None, draft=False): if cumulative: # not to mess up with the original test dataset cr = deepcopy(cr) - cr['cumulative_state'] = 1 + cr["cumulative_state"] = 1 if draft: - params = 'draft' + params = "draft" else: - params = '' + params = "" - response = self.client.post(f'/rest/v2/datasets?{params}', cr, format='json') + response = self.client.post(f"/rest/v2/datasets?{params}", cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data['api_meta']['version'], 2) + self.assertEqual(response.data["api_meta"]["version"], 2) - cr = CatalogRecord.objects.get(pk=response.data['id']) - self.assertEqual(cr.api_meta['version'], 2, 'api_version should be 2') + cr = CatalogRecord.objects.get(pk=response.data["id"]) + self.assertEqual(cr.api_meta["version"], 2, "api_version should be 2") return response.data def _assert_api_version(self, identifier, version): - response = self.client.get(f'/rest/datasets/{identifier}') + response = self.client.get(f"/rest/datasets/{identifier}") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['api_meta']['version'], version, 'api_version should have been changed') + self.assertEqual( + response.data["api_meta"]["version"], + version, + "api_version should have been changed", + ) def test_datasets_are_assigned_to_correct_api_version_on_create(self): """ @@ -90,48 +95,85 @@ def test_version_v2_blocks_all_v1_apis(self): cr_v2 = self._create_v2_dataset() - cr_v2['research_dataset']['title']['en'] = 'totally unique changes to the english title' + cr_v2["research_dataset"]["title"]["en"] = "totally unique changes to the english title" # test basic update operations - response = self.client.put(f'/rest/datasets/{cr_v2["id"]}', cr_v2, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'v1 modifications should have been blocked') - self.assertTrue('correct api version' in response.data['detail'][0], 'msg should be about api version') - - response = self.client.patch(f'/rest/datasets/{cr_v2["id"]}', cr_v2, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'v1 modifications should have been blocked') - self.assertTrue('correct api version' in response.data['detail'][0], 'msg should be about api version') + response = self.client.put(f'/rest/datasets/{cr_v2["id"]}', cr_v2, format="json") + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + "v1 modifications should have been blocked", + ) + self.assertTrue( + "correct api version" in response.data["detail"][0], + "msg should be about api version", + ) + + response = self.client.patch(f'/rest/datasets/{cr_v2["id"]}', cr_v2, format="json") + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + "v1 modifications should have been blocked", + ) + self.assertTrue( + "correct api version" in response.data["detail"][0], + "msg should be about api version", + ) cr_v1 = self._create_v1_dataset() - cr_v1['research_dataset']['title']['en'] = 'this title update should be file' - - response = self.client.put('/rest/datasets', [cr_v1, cr_v2], format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'v1 modifications should have been blocked') - self.assertTrue('correct api version' in response.data['detail'][0], 'msg should be about api version') - - response = self.client.patch('/rest/datasets', [cr_v1, cr_v2], format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'v1 modifications should have been blocked') - self.assertTrue('correct api version' in response.data['detail'][0], 'msg should be about api version') + cr_v1["research_dataset"]["title"]["en"] = "this title update should be file" + + response = self.client.put("/rest/datasets", [cr_v1, cr_v2], format="json") + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + "v1 modifications should have been blocked", + ) + self.assertTrue( + "correct api version" in response.data["detail"][0], + "msg should be about api version", + ) + + response = self.client.patch("/rest/datasets", [cr_v1, cr_v2], format="json") + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + "v1 modifications should have been blocked", + ) + self.assertTrue( + "correct api version" in response.data["detail"][0], + "msg should be about api version", + ) # test change_cumulative_state params = f'identifier={cr_v2["identifier"]}&cumulative_state=1' - response = self.client.post(f'/rpc/datasets/change_cumulative_state?{params}', format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'v1 modifications should have been blocked') - self.assertTrue('correct api version' in response.data['detail'][0], 'msg should be about api version') + response = self.client.post( + f"/rpc/datasets/change_cumulative_state?{params}", format="json" + ) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + "v1 modifications should have been blocked", + ) + self.assertTrue( + "correct api version" in response.data["detail"][0], + "msg should be about api version", + ) # add directory with some files in it to test other rpc apis # dir contains first 5 files 'pid:urn:n' cr_dirs = deepcopy(self.cr_test_data) - cr_dirs['research_dataset']['directories'] = [ + cr_dirs["research_dataset"]["directories"] = [ { "title": "dir_name", "identifier": "pid:urn:dir:3", "description": "What is in this directory", "use_category": { "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/method" - } + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/method", + }, } ] @@ -142,19 +184,35 @@ def test_version_v2_blocks_all_v1_apis(self): # the directory does not have anything to add but it is not relevant here, since # api should return error about the api version before that params = f'cr_identifier={cr_dirs["identifier"]}&dir_identifier=pid:urn:dir:3' - response = self.client.post(f'/rpc/datasets/refresh_directory_content?{params}', format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'v1 modifications should have been blocked') - self.assertTrue('correct api version' in response.data['detail'][0], 'msg should be about api version') + response = self.client.post( + f"/rpc/datasets/refresh_directory_content?{params}", format="json" + ) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + "v1 modifications should have been blocked", + ) + self.assertTrue( + "correct api version" in response.data["detail"][0], + "msg should be about api version", + ) # test fix_deprecated - response = self.client.delete('/rest/files/pid:urn:1') + response = self.client.delete("/rest/files/pid:urn:1") self.assertEqual(response.status_code, status.HTTP_200_OK) params = f'identifier={cr_dirs["identifier"]}' - response = self.client.post(f'/rpc/datasets/fix_deprecated?{params}', format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'v1 modifications should have been blocked') - self.assertTrue('correct api version' in response.data['detail'][0], 'msg should be about api version') + response = self.client.post(f"/rpc/datasets/fix_deprecated?{params}", format="json") + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + "v1 modifications should have been blocked", + ) + self.assertTrue( + "correct api version" in response.data["detail"][0], + "msg should be about api version", + ) def test_v2_rest_api_modification_updates_api_version(self): """ @@ -164,34 +222,34 @@ def test_v2_rest_api_modification_updates_api_version(self): # test basic single PUT/PATCH updates - for http_verb in ['put', 'patch']: + for http_verb in ["put", "patch"]: update_request = getattr(self.client, http_verb) cr_v1 = self._create_v1_dataset() - cr_v1['research_dataset']['title']['en'] = 'some new title' + cr_v1["research_dataset"]["title"]["en"] = "some new title" - response = update_request(f'/rest/v2/datasets/{cr_v1["id"]}', cr_v1, format='json') + response = update_request(f'/rest/v2/datasets/{cr_v1["id"]}', cr_v1, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_api_version(cr_v1['identifier'], 2) + self._assert_api_version(cr_v1["identifier"], 2) # test basic bulk PUT/PATCH updates - for http_verb in ['put', 'patch']: + for http_verb in ["put", "patch"]: update_request = getattr(self.client, http_verb) cr_v1_0 = self._create_v1_dataset() cr_v1_1 = self._create_v1_dataset() - cr_v1_0['research_dataset']['title']['en'] = 'some new title' - cr_v1_1['research_dataset']['title']['en'] = 'some new title for another' + cr_v1_0["research_dataset"]["title"]["en"] = "some new title" + cr_v1_1["research_dataset"]["title"]["en"] = "some new title for another" - response = update_request('/rest/v2/datasets', [cr_v1_0, cr_v1_1], format='json') + response = update_request("/rest/v2/datasets", [cr_v1_0, cr_v1_1], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_api_version(cr_v1_0['identifier'], 2) - self._assert_api_version(cr_v1_1['identifier'], 2) + self._assert_api_version(cr_v1_0["identifier"], 2) + self._assert_api_version(cr_v1_1["identifier"], 2) # test POST /rest/v2/datasets/{PID}/files updates api version @@ -199,43 +257,47 @@ def test_v2_rest_api_modification_updates_api_version(self): cr_v1 = self._create_v1_dataset(cumulative=True) file_changes = { - 'files': [ - { 'identifier': 'pid:urn:1'}, # adds file, but entry is otherwise not persisted - { 'identifier': 'pid:urn:2', 'title': 'custom title', 'use_category': { 'identifier': 'source' } }, + "files": [ + {"identifier": "pid:urn:1"}, # adds file, but entry is otherwise not persisted + { + "identifier": "pid:urn:2", + "title": "custom title", + "use_category": {"identifier": "source"}, + }, ] } - response = self.client.post(f'/rest/v2/datasets/{cr_v1["id"]}/files', file_changes, format='json') + response = self.client.post( + f'/rest/v2/datasets/{cr_v1["id"]}/files', file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_api_version(cr_v1['id'], 2) + self._assert_api_version(cr_v1["id"], 2) # test PUT/PATCH /rest/v2/datasets/{PID}/files/user_metadata - for http_verb in ['put', 'patch']: + for http_verb in ["put", "patch"]: update_request = getattr(self.client, http_verb) cr = self.cr_test_data - cr['research_dataset']['files'] = [ + cr["research_dataset"]["files"] = [ { "title": "customtitle", "identifier": "pid:urn:1", "use_category": { "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source" - } + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source", + }, }, ] cr_v1 = self._create_v1_dataset(cr) user_metadata = { - 'files': [ + "files": [ { - 'identifier': 'pid:urn:1', - 'title': 'custom title 1', - 'use_category': { - 'identifier': 'source' - } + "identifier": "pid:urn:1", + "title": "custom title 1", + "use_category": {"identifier": "source"}, } ] } @@ -243,11 +305,11 @@ def test_v2_rest_api_modification_updates_api_version(self): response = update_request( f'/rest/v2/datasets/{cr_v1["id"]}/files/user_metadata', user_metadata, - format='json' + format="json", ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_api_version(cr_v1['id'], 2) + self._assert_api_version(cr_v1["id"], 2) def test_v2_rpc_api_modification_updates_api_version(self): """ @@ -260,18 +322,22 @@ def test_v2_rpc_api_modification_updates_api_version(self): # create one non-cumulative dataset without files so it can be edited with this rpc cr_v1_non_cum = self.cr_test_data - cr_v1_non_cum['cumulative_state'] = 0 - del cr_v1_non_cum['research_dataset']['files'] + cr_v1_non_cum["cumulative_state"] = 0 + del cr_v1_non_cum["research_dataset"]["files"] cr_v1_non_cum = self._create_v1_dataset(cr=cr_v1_non_cum) params = f'identifier={cr_v1_non_cum["identifier"]}&cumulative_state=1' - response = self.client.post(f'/rpc/v2/datasets/change_cumulative_state?{params}', format='json') + response = self.client.post( + f"/rpc/v2/datasets/change_cumulative_state?{params}", format="json" + ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) self._assert_api_version(cr_v1_non_cum["identifier"], 2) params = f'identifier={cr_v1_cum["identifier"]}&cumulative_state=2' - response = self.client.post(f'/rpc/v2/datasets/change_cumulative_state?{params}', format='json') + response = self.client.post( + f"/rpc/v2/datasets/change_cumulative_state?{params}", format="json" + ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) self._assert_api_version(cr_v1_cum["identifier"], 2) @@ -280,15 +346,15 @@ def test_v2_rpc_api_modification_updates_api_version(self): cr_v1 = self._create_v1_dataset() params = f'identifier={cr_v1["identifier"]}' - response = self.client.post(f'/rpc/v2/datasets/create_draft?{params}', format='json') + response = self.client.post(f"/rpc/v2/datasets/create_draft?{params}", format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) response = self.client.get(f'/rest/datasets/{cr_v1["identifier"]}') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr_v1 = response.data - new_id = response.data['identifier'] - response = self.client.get(f'/rest/datasets/{new_id}') + new_id = response.data["identifier"] + response = self.client.get(f"/rest/datasets/{new_id}") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) new_dataset = response.data @@ -299,15 +365,15 @@ def test_v2_rpc_api_modification_updates_api_version(self): cr_v1 = self._create_v1_dataset() params = f'identifier={cr_v1["identifier"]}' - response = self.client.post(f'/rpc/v2/datasets/create_new_version?{params}', format='json') + response = self.client.post(f"/rpc/v2/datasets/create_new_version?{params}", format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) response = self.client.get(f'/rest/datasets/{cr_v1["identifier"]}') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr_v1 = response.data - new_id = response.data['identifier'] - response = self.client.get(f'/rest/datasets/{new_id}') + new_id = response.data["identifier"] + response = self.client.get(f"/rest/datasets/{new_id}") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) new_dataset = response.data diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py b/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py index bd09cf74..710ecfef 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py @@ -21,10 +21,12 @@ DFT_CATALOG = django_settings.DFT_DATA_CATALOG_IDENTIFIER ATT_CATALOG = django_settings.ATT_DATA_CATALOG_IDENTIFIER + class CatalogRecordDraftTests(CatalogRecordApiWriteCommon): """ Tests related to draft datasets """ + def setUp(self): super().setUp() @@ -32,87 +34,90 @@ def setUp(self): dc = DataCatalog.objects.get(pk=1) catalog_json = dc.catalog_json for identifier in END_USER_ALLOWED_DATA_CATALOGS: - catalog_json['identifier'] = identifier + catalog_json["identifier"] = identifier # not all non-draft catalogs are actually ida but that is not tested here - catalog_json['research_dataset_schema'] = 'dft' if identifier == DFT_CATALOG else 'ida' + catalog_json["research_dataset_schema"] = "dft" if identifier == DFT_CATALOG else "ida" dc = DataCatalog.objects.create( catalog_json=catalog_json, date_created=get_tz_aware_now_without_micros(), - catalog_record_services_create='testuser,api_auth_user,metax', - catalog_record_services_edit='testuser,api_auth_user,metax', - catalog_record_services_read='testuser,api_auth_user,metax' + catalog_record_services_create="testuser,api_auth_user,metax", + catalog_record_services_edit="testuser,api_auth_user,metax", + catalog_record_services_read="testuser,api_auth_user,metax", ) self.minimal_draft = { "metadata_provider_org": "abc-org-123", "metadata_provider_user": "abc-usr-123", - "research_dataset": { - "title": { - "en": "Wonderful Title" - } - } + "research_dataset": {"title": {"en": "Wonderful Title"}}, } self.token = get_test_oidc_token(new_proxy=True) self._mock_token_validation_succeeds() # Create published record with owner: testuser and pk 1 # Create draft records with owner: testuser, pk: 2 and owner: 'some owner who is not you', pk 3 - self._set_cr_owner_and_state(1, 'published', self.token['CSCUserName']) # Published dataset - self.assertEqual(CatalogRecordV2.objects.get(pk=1).metadata_provider_user, 'testuser') + self._set_cr_owner_and_state(1, "published", self.token["CSCUserName"]) # Published dataset + self.assertEqual(CatalogRecordV2.objects.get(pk=1).metadata_provider_user, "testuser") - self._set_cr_owner_and_state(2, 'draft', self.token['CSCUserName']) # testusers' draft - self.assertEqual(CatalogRecordV2.objects.get(pk=2).metadata_provider_user, 'testuser') + self._set_cr_owner_and_state(2, "draft", self.token["CSCUserName"]) # testusers' draft + self.assertEqual(CatalogRecordV2.objects.get(pk=2).metadata_provider_user, "testuser") - self._set_cr_owner_and_state(3, 'draft', '#### Some owner who is not you ####') # Draft dataset for some user - self.assertNotEqual(CatalogRecordV2.objects.get(pk=3).metadata_provider_user, 'testuser') + self._set_cr_owner_and_state( + 3, "draft", "#### Some owner who is not you ####" + ) # Draft dataset for some user + self.assertNotEqual(CatalogRecordV2.objects.get(pk=3).metadata_provider_user, "testuser") def _set_cr_owner_and_state(self, cr_id, state, owner): - ''' helper method for testing user accessibility for draft datasets ''' + """ helper method for testing user accessibility for draft datasets """ cr = CatalogRecordV2.objects.get(pk=cr_id) cr.state = state cr.user_created = owner cr.metadata_provider_user = owner - cr.editor = None # pretend the record was created by user directly - cr.data_catalog_id = DataCatalog.objects.get(catalog_json__identifier=END_USER_ALLOWED_DATA_CATALOGS[0]).id + cr.data_catalog_id = DataCatalog.objects.get( + catalog_json__identifier=END_USER_ALLOWED_DATA_CATALOGS[0] + ).id cr.force_save() def test_field_state_exists(self): """Try fetching any dataset, field 'state' should be returned'""" - cr = self.client.get('/rest/v2/datasets/13').data - self.assertEqual('state' in cr, True) + cr = self.client.get("/rest/v2/datasets/13").data + self.assertEqual("state" in cr, True) def _test_issued_date_is_not_generated_for_drafts(self): - ''' + """ Drafts will not have the issued date generated Field is created when dataset is published - ''' + """ # Dataset without issued date - self.cr_full_ida_test_data['research_dataset'].pop('issued', None) + self.cr_full_ida_test_data["research_dataset"].pop("issued", None) # Create draft - response = self.client.post('/rest/v2/datasets?draft=true', self.cr_full_ida_test_data, format="json") + response = self.client.post( + "/rest/v2/datasets?draft=true", self.cr_full_ida_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue('issued' not in response.data['research_dataset'], response.data) + self.assertTrue("issued" not in response.data["research_dataset"], response.data) # Issued_date is generated when dataset is published - publish = self.client.post('/rpc/v2/datasets/publish_dataset?identifier={}'.format(response.data['identifier'])) + publish = self.client.post( + "/rpc/v2/datasets/publish_dataset?identifier={}".format(response.data["identifier"]) + ) self.assertEqual(publish.status_code, status.HTTP_200_OK, publish.data) - published = self.client.get('/rest/v2/datasets/{}'.format(response.data['identifier'])) + published = self.client.get("/rest/v2/datasets/{}".format(response.data["identifier"])) self.assertEqual(published.status_code, status.HTTP_200_OK, published.data) - self.assertTrue('issued' in published.data['research_dataset'], published.data) + self.assertTrue("issued" in published.data["research_dataset"], published.data) def test_change_state_field_through_API(self): """Fetch a dataset and change its state. - Value should remain: 'published' """ + Value should remain: 'published'""" - cr = self.client.get('/rest/v2/datasets/1').data - cr['state'] = 'changed value' - response = self.client.put('/rest/v2/datasets/1', cr, format="json") + cr = self.client.get("/rest/v2/datasets/1").data + cr["state"] = "changed value" + response = self.client.put("/rest/v2/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertFalse(response.data['state'] == 'changed value') + self.assertFalse(response.data["state"] == "changed value") ### # Tests for different user roles access to drafts @@ -120,56 +125,56 @@ def test_change_state_field_through_API(self): @responses.activate def test_endusers_access_to_draft_datasets(self): - ''' End user should get published data and his/her drafts ''' + """ End user should get published data and his/her drafts """ # Test access as end user - self._use_http_authorization(method='bearer', token=self.token) + self._use_http_authorization(method="bearer", token=self.token) # Test access for owner of dataset - response = self.client.get('/rest/v2/datasets/1') + response = self.client.get("/rest/v2/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.status_code) - response = self.client.get('/rest/v2/datasets/2') + response = self.client.get("/rest/v2/datasets/2") self.assertEqual(response.status_code, status.HTTP_200_OK, response.status_code) - response = self.client.get('/rest/v2/datasets/3') + response = self.client.get("/rest/v2/datasets/3") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.status_code) # Test for multiple datasets - response = self.client.get('/rest/v2/datasets', format="json") + response = self.client.get("/rest/v2/datasets", format="json") # Returned list of datasets should not have owner "#### Some owner who is not you ####" - owners = [cr['metadata_provider_user'] for cr in response.data['results']] - self.assertEqual('#### Some owner who is not you ####' not in owners, True, response.data) + owners = [cr["metadata_provider_user"] for cr in response.data["results"]] + self.assertEqual("#### Some owner who is not you ####" not in owners, True, response.data) def test_service_users_access_to_draft_datasets(self): - ''' Service users should get all data ''' + """ Service users should get all data """ # test access as a service-user - self._use_http_authorization(method='basic', username='metax') + self._use_http_authorization(method="basic", username="metax") - response = self.client.get('/rest/v2/datasets/1') + response = self.client.get("/rest/v2/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.status_code) - response = self.client.get('/rest/v2/datasets/2') + response = self.client.get("/rest/v2/datasets/2") self.assertEqual(response.status_code, status.HTTP_200_OK, response.status_code) - response = self.client.get('/rest/v2/datasets/3') + response = self.client.get("/rest/v2/datasets/3") self.assertEqual(response.status_code, status.HTTP_200_OK, response.status_code) # test for multiple datasets - response = self.client.get('/rest/v2/datasets', format="json") + response = self.client.get("/rest/v2/datasets", format="json") # Returned list of datasets should have owner "#### Some owner who is not you ####" - owners = [cr['metadata_provider_user'] for cr in response.data['results']] - self.assertEqual('#### Some owner who is not you ####' in owners, True, response.data) + owners = [cr["metadata_provider_user"] for cr in response.data["results"]] + self.assertEqual("#### Some owner who is not you ####" in owners, True, response.data) def test_anonymous_users_access_to_draft_datasets(self): - ''' Unauthenticated user should get only published datasets ''' + """ Unauthenticated user should get only published datasets """ # Test access as unauthenticated user self.client._credentials = {} - response = self.client.get('/rest/v2/datasets/1') + response = self.client.get("/rest/v2/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.get('/rest/v2/datasets/2') + response = self.client.get("/rest/v2/datasets/2") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) - response = self.client.get('/rest/v2/datasets/3') + response = self.client.get("/rest/v2/datasets/3") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) # test for multiple datasets - response = self.client.get('/rest/v2/datasets', format="json") + response = self.client.get("/rest/v2/datasets", format="json") # Returned list of datasets should not have drafts - states = [cr['state'] for cr in response.data['results']] - self.assertEqual('draft' not in states, True, response.data) + states = [cr["state"] for cr in response.data["results"]] + self.assertEqual("draft" not in states, True, response.data) ### # Tests for different user roles access to update drafts @@ -177,76 +182,82 @@ def test_anonymous_users_access_to_draft_datasets(self): @responses.activate def test_endusers_can_update_draft_datasets(self): - ''' End user should be able to update only his/her drafts ''' + """ End user should be able to update only his/her drafts """ # Set end user - self._use_http_authorization(method='bearer', token=self.token) + self._use_http_authorization(method="bearer", token=self.token) - for http_verb in ['put', 'patch']: + for http_verb in ["put", "patch"]: update_request = getattr(self.client, http_verb) - data1 = self.client.get('/rest/v2/datasets/1').data # published - response = update_request('/rest/v2/datasets/1', data1, format="json") + data1 = self.client.get("/rest/v2/datasets/1").data # published + response = update_request("/rest/v2/datasets/1", data1, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - data2 = self.client.get('/rest/v2/datasets/2').data # end users own draft - response = update_request('/rest/v2/datasets/2', data2, format="json") + data2 = self.client.get("/rest/v2/datasets/2").data # end users own draft + response = update_request("/rest/v2/datasets/2", data2, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - data3 = self.client.get('/rest/v2/datasets/3').data # someone elses draft - response = update_request('/rest/v2/datasets/3', data3, format="json") + data3 = self.client.get("/rest/v2/datasets/3").data # someone elses draft + response = update_request("/rest/v2/datasets/3", data3, format="json") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) # test for multiple datasets - response = update_request('/rest/v2/datasets', [data1, data2, data3], format="json") - owners = [cr['object']['metadata_provider_user'] for cr in response.data['success']] - self.assertEqual('#### Some owner who is not you ####' not in owners, True, response.data) + response = update_request("/rest/v2/datasets", [data1, data2, data3], format="json") + owners = [cr["object"]["metadata_provider_user"] for cr in response.data["success"]] + self.assertEqual( + "#### Some owner who is not you ####" not in owners, True, response.data + ) def test_service_users_can_update_draft_datasets(self): - ''' Dataset drafts should be able to be updated by service users (service is responsible that - their current user in e.g. Qvain is allowed to access the dataset)''' + """Dataset drafts should be able to be updated by service users (service is responsible that + their current user in e.g. Qvain is allowed to access the dataset)""" # Set service-user - self._use_http_authorization(method='basic', username='metax') + self._use_http_authorization(method="basic", username="metax") - for http_verb in ['put', 'patch']: + for http_verb in ["put", "patch"]: update_request = getattr(self.client, http_verb) - data1 = self.client.get('/rest/v2/datasets/1').data # published - response = update_request('/rest/v2/datasets/1', data1, format="json") + data1 = self.client.get("/rest/v2/datasets/1").data # published + response = update_request("/rest/v2/datasets/1", data1, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - data2 = self.client.get('/rest/v2/datasets/2').data # draft - response = update_request('/rest/v2/datasets/2', data2, format="json") + data2 = self.client.get("/rest/v2/datasets/2").data # draft + response = update_request("/rest/v2/datasets/2", data2, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - data3 = self.client.get('/rest/v2/datasets/3').data # draft - response = update_request('/rest/v2/datasets/3', data3, format="json") + data3 = self.client.get("/rest/v2/datasets/3").data # draft + response = update_request("/rest/v2/datasets/3", data3, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # test for multiple datasets - response = update_request('/rest/v2/datasets', [data1, data2, data3], format="json") - self.assertEqual(len(response.data['success']), 3, 'response.data should contain 3 changed objects') - owners = [cr['object']['metadata_provider_user'] for cr in response.data['success']] - self.assertEqual('#### Some owner who is not you ####' in owners, True, response.data) + response = update_request("/rest/v2/datasets", [data1, data2, data3], format="json") + self.assertEqual( + len(response.data["success"]), + 3, + "response.data should contain 3 changed objects", + ) + owners = [cr["object"]["metadata_provider_user"] for cr in response.data["success"]] + self.assertEqual("#### Some owner who is not you ####" in owners, True, response.data) def test_anonymous_user_cannot_update_draft_datasets(self): - ''' Unauthenticated user should not be able to know drafts exists in the first place''' + """ Unauthenticated user should not be able to know drafts exists in the first place""" # Set unauthenticated user self.client._credentials = {} # Fetches a published dataset since unauthenticated user can't get drafts - response = self.client.get('/rest/v2/datasets/1') + response = self.client.get("/rest/v2/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) data = response.data - for http_verb in ['put', 'patch']: + for http_verb in ["put", "patch"]: update_request = getattr(self.client, http_verb) - response = update_request('/rest/v2/datasets/1', data, format="json") # published + response = update_request("/rest/v2/datasets/1", data, format="json") # published self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.status_code) - response = update_request('/rest/v2/datasets/2', data, format="json") # draft + response = update_request("/rest/v2/datasets/2", data, format="json") # draft self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.status_code) - response = update_request('/rest/v2/datasets/3', data, format="json") # draft + response = update_request("/rest/v2/datasets/3", data, format="json") # draft self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.status_code) # test for multiple datasets - response = update_request('/rest/v2/datasets', data, format="json") + response = update_request("/rest/v2/datasets", data, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.status_code) ### @@ -254,25 +265,25 @@ def test_anonymous_user_cannot_update_draft_datasets(self): ### def test_draft_is_permanently_deleted_by_service_user(self): - '''Draft datasets should be permanently deleted from the database. - Only the dataset owner is able to delete draft datasets.''' + """Draft datasets should be permanently deleted from the database. + Only the dataset owner is able to delete draft datasets.""" # Set service-user - self._use_http_authorization(method='basic', username='metax') + self._use_http_authorization(method="basic", username="metax") for cr_id in (2, 3): - response = self.client.delete('/rest/v2/datasets/%d' % cr_id) + response = self.client.delete("/rest/v2/datasets/%d" % cr_id) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) self.assertFalse(CatalogRecordV2.objects_unfiltered.filter(pk=cr_id).exists()) @responses.activate def test_draft_is_permanently_deleted_by_enduser(self): # Set end user - self._use_http_authorization(method='bearer', token=self.token) + self._use_http_authorization(method="bearer", token=self.token) - response = self.client.delete('/rest/v2/datasets/2') + response = self.client.delete("/rest/v2/datasets/2") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) self.assertFalse(CatalogRecordV2.objects_unfiltered.filter(pk=2).exists()) - response = self.client.delete('/rest/v2/datasets/3') + response = self.client.delete("/rest/v2/datasets/3") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) ### @@ -280,147 +291,179 @@ def test_draft_is_permanently_deleted_by_enduser(self): ### def test_service_users_can_save_draft_datasets(self): - ''' Drafts should be saved without preferred identifier ''' + """ Drafts should be saved without preferred identifier """ # test access as a service-user - self._use_http_authorization(method='basic', username='metax') + self._use_http_authorization(method="basic", username="metax") - response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets?draft", self.cr_test_data, format="json") - pid = response.data['research_dataset']['preferred_identifier'] + pid = response.data["research_dataset"]["preferred_identifier"] self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue(pid == 'draft:%s' % response.data['identifier'], response.data) - self.assertTrue('urn' not in pid, response.data) - self.assertTrue('doi' not in pid, response.data) - self.assertTrue(response.data['state'] == 'draft', response.data) - - for queryparam in ('', '?draft=false'): - response = self.client.post('/rest/v2/datasets{}'.format(queryparam), self.cr_test_data, format="json") + self.assertTrue(pid == "draft:%s" % response.data["identifier"], response.data) + self.assertTrue("urn" not in pid, response.data) + self.assertTrue("doi" not in pid, response.data) + self.assertTrue(response.data["state"] == "draft", response.data) + + for queryparam in ("", "?draft=false"): + response = self.client.post( + "/rest/v2/datasets{}".format(queryparam), + self.cr_test_data, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue(response.data['state'] == 'published', response.data) + self.assertTrue(response.data["state"] == "published", response.data) ### # Tests for use_doi_for_published -field ### def test_use_doi_for_published_field(self): - ''' Drafts with 'use_doi' checkbox checked should have 'use_doi_for_published' == True - to tell that pid will be of type DOI when draft is published ''' - - self.cr_test_data['data_catalog'] = IDA_CATALOG - response = self.client.post('/rest/v2/datasets?pid_type=doi&draft=true', self.cr_test_data, format="json") + """Drafts with 'use_doi' checkbox checked should have 'use_doi_for_published' == True + to tell that pid will be of type DOI when draft is published""" + + self.cr_test_data["data_catalog"] = IDA_CATALOG + response = self.client.post( + "/rest/v2/datasets?pid_type=doi&draft=true", + self.cr_test_data, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertTrue('use_doi_for_published' in response.data) - self.assertTrue(response.data['use_doi_for_published'] is True, response.data) + self.assertTrue("use_doi_for_published" in response.data) + self.assertTrue(response.data["use_doi_for_published"] is True, response.data) # update draft & toggle use_doi_for_published-field - identifier = response.data['identifier'] + identifier = response.data["identifier"] - response.data['use_doi_for_published'] = False - update = self.client.put(f'/rest/v2/datasets/{identifier}', response.data, format="json") + response.data["use_doi_for_published"] = False + update = self.client.put(f"/rest/v2/datasets/{identifier}", response.data, format="json") self.assertEqual(update.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['use_doi_for_published'] is False, response.data) + self.assertTrue(response.data["use_doi_for_published"] is False, response.data) - update.data['use_doi_for_published'] = True - toggle = self.client.put(f'/rest/v2/datasets/{identifier}', update.data, format="json") - self.assertTrue(toggle.data['use_doi_for_published'] is True, toggle.data) + update.data["use_doi_for_published"] = True + toggle = self.client.put(f"/rest/v2/datasets/{identifier}", update.data, format="json") + self.assertTrue(toggle.data["use_doi_for_published"] is True, toggle.data) # Get the dataset afterwards and check that new field is there - response = self.client.get(f'/rest/v2/datasets/{identifier}', format='json') - self.assertTrue('use_doi_for_published' in response.data, response.data) + response = self.client.get(f"/rest/v2/datasets/{identifier}", format="json") + self.assertTrue("use_doi_for_published" in response.data, response.data) # publish the draft - publish = self.client.post(f'/rpc/v2/datasets/publish_dataset?identifier={identifier}', format="json") + publish = self.client.post( + f"/rpc/v2/datasets/publish_dataset?identifier={identifier}", format="json" + ) self.assertEqual(publish.status_code, status.HTTP_200_OK, publish.data) # Published dataset should not return 'use_doi_for_published' # PID should be of type DOI when dataset is published - response = self.client.get(f'/rest/v2/datasets/{identifier}?include_user_metadata', format='json') + response = self.client.get( + f"/rest/v2/datasets/{identifier}?include_user_metadata", format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue('use_doi_for_published' not in response.data) - self.assertTrue('doi' in response.data['research_dataset']['preferred_identifier'], response.data) + self.assertTrue("use_doi_for_published" not in response.data) + self.assertTrue( + "doi" in response.data["research_dataset"]["preferred_identifier"], + response.data, + ) def test_use_doi_for_published_field_not_in_use(self): - ''' Drafts with 'use_doi' checkbox unchecked should have - pid of type URN when draft is published ''' - - for call in ('/rest/v2/datasets?pid_type=urn&draft=true', '/rest/v2/datasets?draft=true'): - self.cr_test_data['data_catalog'] = IDA_CATALOG + """Drafts with 'use_doi' checkbox unchecked should have + pid of type URN when draft is published""" + + for call in ( + "/rest/v2/datasets?pid_type=urn&draft=true", + "/rest/v2/datasets?draft=true", + ): + self.cr_test_data["data_catalog"] = IDA_CATALOG response = self.client.post(call, self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertTrue('use_doi_for_published' in response.data) - self.assertTrue(response.data['use_doi_for_published'] is False, response.data) + self.assertTrue("use_doi_for_published" in response.data) + self.assertTrue(response.data["use_doi_for_published"] is False, response.data) # update draft & toggle use_doi_for_published-field - identifier = response.data['identifier'] + identifier = response.data["identifier"] - response.data['use_doi_for_published'] = True - update = self.client.put(f'/rest/v2/datasets/{identifier}', response.data, format="json") - self.assertTrue(response.data['use_doi_for_published'] is True, response.data) + response.data["use_doi_for_published"] = True + update = self.client.put( + f"/rest/v2/datasets/{identifier}", response.data, format="json" + ) + self.assertTrue(response.data["use_doi_for_published"] is True, response.data) - update.data['use_doi_for_published'] = False - toggle = self.client.put(f'/rest/v2/datasets/{identifier}', update.data, format="json") - self.assertTrue(toggle.data['use_doi_for_published'] is False, toggle.data) + update.data["use_doi_for_published"] = False + toggle = self.client.put(f"/rest/v2/datasets/{identifier}", update.data, format="json") + self.assertTrue(toggle.data["use_doi_for_published"] is False, toggle.data) # publish the draft - publish = self.client.post(f'/rpc/v2/datasets/publish_dataset?identifier={identifier}', format="json") + publish = self.client.post( + f"/rpc/v2/datasets/publish_dataset?identifier={identifier}", + format="json", + ) self.assertEqual(publish.status_code, status.HTTP_200_OK, publish.data) - response = self.client.get(f'/rest/v2/datasets/{identifier}?include_user_metadata', format='json') - self.assertTrue('use_doi_for_published' not in response.data) - self.assertTrue('urn' in response.data['research_dataset']['preferred_identifier'], response.data) + response = self.client.get( + f"/rest/v2/datasets/{identifier}?include_user_metadata", format="json" + ) + self.assertTrue("use_doi_for_published" not in response.data) + self.assertTrue( + "urn" in response.data["research_dataset"]["preferred_identifier"], + response.data, + ) ### # Tests for draft data catalog ### def test_minimal_draft_dataset_creation(self): - ''' Drafts have different requirements for mandatory fields ''' - self._use_http_authorization(method='basic', username='metax') + """ Drafts have different requirements for mandatory fields """ + self._use_http_authorization(method="basic", username="metax") - response = self.client.post('/rest/v2/datasets?draft', self.minimal_draft, format="json") + response = self.client.post("/rest/v2/datasets?draft", self.minimal_draft, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def test_allow_files_and_dirs_in_draft_catalog(self): - ''' Files can be added to datasets that are in draft catalog ''' - self._use_http_authorization(method='basic', username='metax') - - for type in ['files', 'directories']: - self.minimal_draft['research_dataset'][type] = [ - { - "identifier": "pid:urn:{}1".format('' if type == 'files' else 'dir:') - } + """ Files can be added to datasets that are in draft catalog """ + self._use_http_authorization(method="basic", username="metax") + + for type in ["files", "directories"]: + self.minimal_draft["research_dataset"][type] = [ + {"identifier": "pid:urn:{}1".format("" if type == "files" else "dir:")} ] - response = self.client.post('/rest/v2/datasets?draft', self.minimal_draft, format="json") + response = self.client.post( + "/rest/v2/datasets?draft", self.minimal_draft, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - response = self.client.get(f'/rest/v2/datasets/{response.data["id"]}/files', format="json") + response = self.client.get( + f'/rest/v2/datasets/{response.data["id"]}/files', format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertTrue(response.data, response.data) - self.minimal_draft['research_dataset'].pop(type) + self.minimal_draft["research_dataset"].pop(type) def test_publish_in_draft_catalog_is_not_allowed(self): - self._use_http_authorization(method='basic', username='metax') + self._use_http_authorization(method="basic", username="metax") - del self.cr_test_data['data_catalog'] - del self.cr_test_data['research_dataset']['files'] + del self.cr_test_data["data_catalog"] + del self.cr_test_data["research_dataset"]["files"] - response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets?draft", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr = response.data - response = self.client.post(f'/rpc/v2/datasets/publish_dataset?identifier={cr["id"]}', format="json") + response = self.client.post( + f'/rpc/v2/datasets/publish_dataset?identifier={cr["id"]}', format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_prevent_update_published_dataset_to_draft_catalog(self): - self._use_http_authorization(method='basic', username='metax') + self._use_http_authorization(method="basic", username="metax") - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr = response.data - cr['data_catalog'] = {"identifier": DFT_CATALOG} + cr["data_catalog"] = {"identifier": DFT_CATALOG} response = self.client.put(f'/rest/v2/datasets/{cr["id"]}', cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) @@ -429,31 +472,29 @@ def test_allow_remote_resources_in_ida_for_drafts(self): """ When dataset is in draft state, it should be validated with dft catalog """ - self.cr_test_data['data_catalog'] = {"identifier": IDA_CATALOG} - self.cr_test_data['research_dataset']['remote_resources'] = [ - { - "title": "some title", - "use_category": {"identifier": "source"} - } + self.cr_test_data["data_catalog"] = {"identifier": IDA_CATALOG} + self.cr_test_data["research_dataset"]["remote_resources"] = [ + {"title": "some title", "use_category": {"identifier": "source"}} ] - response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets?draft", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def test_allow_file_additions_to_drafts(self): """ Files can be added later and the metadata can be modified via RPC apis. """ - files = {"files": [ {"identifier": "pid:urn:5"} ]} + files = {"files": [{"identifier": "pid:urn:5"}]} for catalog in [IDA_CATALOG, ATT_CATALOG, DFT_CATALOG]: - self.cr_test_data['data_catalog'] = {"identifier": catalog} - response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + self.cr_test_data["data_catalog"] = {"identifier": catalog} + response = self.client.post("/rest/v2/datasets?draft", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr = response.data response = self.client.post(f'/rest/v2/datasets/{cr["id"]}/files', files, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['files_added'], 1, response.data) + self.assertEqual(response.data["files_added"], 1, response.data) + class CatalogRecordDraftsOfPublished(CatalogRecordApiWriteCommon): @@ -462,35 +503,41 @@ class CatalogRecordDraftsOfPublished(CatalogRecordApiWriteCommon): """ def _create_dataset(self, cumulative=False, draft=False, with_files=False): - draft = 'true' if draft else 'false' + draft = "true" if draft else "false" cumulative_state = 1 if cumulative else 0 - self.cr_test_data['cumulative_state'] = cumulative_state + self.cr_test_data["cumulative_state"] = cumulative_state if not with_files: - self.cr_test_data['research_dataset'].pop('files', None) - self.cr_test_data['research_dataset'].pop('directories', None) + self.cr_test_data["research_dataset"].pop("files", None) + self.cr_test_data["research_dataset"].pop("directories", None) - response = self.client.post('/rest/v2/datasets?draft=%s' % draft, self.cr_test_data, format="json") + response = self.client.post( + "/rest/v2/datasets?draft=%s" % draft, self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) return response.data def _create_draft(self, id): # create draft - response = self.client.post('/rpc/v2/datasets/create_draft?identifier=%d' % id, format="json") + response = self.client.post( + "/rpc/v2/datasets/create_draft?identifier=%d" % id, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - draft_id = response.data['id'] + draft_id = response.data["id"] # retrieve draft data for modifications - response = self.client.get('/rest/v2/datasets/%s' % draft_id, format="json") + response = self.client.get("/rest/v2/datasets/%s" % draft_id, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('draft_of' in response.data, True, response.data) - self.assertEqual(response.data['draft_of']['id'], id, response.data) + self.assertEqual("draft_of" in response.data, True, response.data) + self.assertEqual(response.data["draft_of"]["id"], id, response.data) return response.data def _merge_draft_changes(self, draft_id): - response = self.client.post('/rpc/v2/datasets/merge_draft?identifier=%d' % draft_id, format="json") + response = self.client.post( + "/rpc/v2/datasets/merge_draft?identifier=%d" % draft_id, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # draft should be permanently destroyed @@ -503,11 +550,15 @@ def test_create_draft_not_locked_before_merge(self): """ cr = self._create_dataset() # create draft - response = self.client.post('/rpc/v2/datasets/create_draft?identifier=%d' % cr['id'], format="json") + response = self.client.post( + "/rpc/v2/datasets/create_draft?identifier=%d" % cr["id"], format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # create draft - response = self.client.post('/rpc/v2/datasets/create_draft?identifier=%d' % cr['id'], format="json") + response = self.client.post( + "/rpc/v2/datasets/create_draft?identifier=%d" % cr["id"], format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_create_and_merge_draft(self): @@ -515,47 +566,51 @@ def test_create_and_merge_draft(self): A simple test to create a draft, change some metadata, and publish the changes. """ cr = self._create_dataset() - initial_title = cr['research_dataset']['title'] + initial_title = cr["research_dataset"]["title"] # create draft - draft_cr = self._create_draft(cr['id']) - draft_cr['research_dataset']['title']['en'] = 'modified title' - draft_cr['preservation_state'] = 20 + draft_cr = self._create_draft(cr["id"]) + draft_cr["research_dataset"]["title"]["en"] = "modified title" + draft_cr["preservation_state"] = 20 # ensure original now has a link to next_draft - response = self.client.get('/rest/v2/datasets/%s' % cr['id'], format="json") + response = self.client.get("/rest/v2/datasets/%s" % cr["id"], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('next_draft' in response.data, True, response.data) - self.assertEqual(response.data['next_draft']['id'], draft_cr['id'], response.data) + self.assertEqual("next_draft" in response.data, True, response.data) + self.assertEqual(response.data["next_draft"]["id"], draft_cr["id"], response.data) # update the draft - response = self.client.put('/rest/v2/datasets/%d' % draft_cr['id'], draft_cr, format="json") + response = self.client.put("/rest/v2/datasets/%d" % draft_cr["id"], draft_cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # ensure original dataset # - does not have the changes yet, since draft has not been published # - has next_draft link, pointing to the preciously created draft - original_cr = CR.objects.get(pk=cr['id']) - self.assertEqual(original_cr.research_dataset['title'], initial_title, original_cr.research_dataset['title']) - self.assertEqual(original_cr.next_draft_id, draft_cr['id']) + original_cr = CR.objects.get(pk=cr["id"]) + self.assertEqual( + original_cr.research_dataset["title"], + initial_title, + original_cr.research_dataset["title"], + ) + self.assertEqual(original_cr.next_draft_id, draft_cr["id"]) # merge draft changes back to original published dataset - self._merge_draft_changes(draft_cr['id']) + self._merge_draft_changes(draft_cr["id"]) # changes should now reflect on original published dataset - response = self.client.get('/rest/v2/datasets/%s' % cr['id'], format="json") + response = self.client.get("/rest/v2/datasets/%s" % cr["id"], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual( - response.data['research_dataset']['title'], - draft_cr['research_dataset']['title'], - response.data + response.data["research_dataset"]["title"], + draft_cr["research_dataset"]["title"], + response.data, ) self.assertEqual( - response.data['preservation_state'], - draft_cr['preservation_state'], - response.data + response.data["preservation_state"], + draft_cr["preservation_state"], + response.data, ) - self.assertEqual('next_draft' in response.data, False, 'next_draft link should be gone') + self.assertEqual("next_draft" in response.data, False, "next_draft link should be gone") def test_missing_issued_date_is_generated_when_draft_is_merged(self): """ @@ -563,26 +618,26 @@ def test_missing_issued_date_is_generated_when_draft_is_merged(self): it back to original published dataset """ cr = self._create_dataset() - initial_issued_date = cr['research_dataset']['issued'] + initial_issued_date = cr["research_dataset"]["issued"] # create draft - draft_cr = self._create_draft(cr['id']) - draft_cr['research_dataset'].pop('issued', None) + draft_cr = self._create_draft(cr["id"]) + draft_cr["research_dataset"].pop("issued", None) # update the draft - response = self.client.put('/rest/v2/datasets/%d' % draft_cr['id'], draft_cr, format="json") + response = self.client.put("/rest/v2/datasets/%d" % draft_cr["id"], draft_cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # merge draft changes back to original published dataset - self._merge_draft_changes(draft_cr['id']) + self._merge_draft_changes(draft_cr["id"]) # changes should now reflect on original published dataset - response = self.client.get('/rest/v2/datasets/%s' % cr['id'], format="json") + response = self.client.get("/rest/v2/datasets/%s" % cr["id"], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertNotEqual( - response.data['research_dataset']['issued'], + response.data["research_dataset"]["issued"], initial_issued_date, - response.data + response.data, ) def test_add_files_to_draft_normal_dataset(self): @@ -590,21 +645,23 @@ def test_add_files_to_draft_normal_dataset(self): Test case where dataset has 0 files in the beginning. """ cr = self._create_dataset(with_files=False) - draft_cr = self._create_draft(cr['id']) + draft_cr = self._create_draft(cr["id"]) # add file to draft - file_changes = { 'files': [{ 'identifier': 'pid:urn:1' }]} - response = self.client.post('/rest/v2/datasets/%d/files' % draft_cr['id'], file_changes, format="json") + file_changes = {"files": [{"identifier": "pid:urn:1"}]} + response = self.client.post( + "/rest/v2/datasets/%d/files" % draft_cr["id"], file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # ensure original has no files - self.assertEqual(CR.objects.get(pk=cr['id']).files.count(), 0) + self.assertEqual(CR.objects.get(pk=cr["id"]).files.count(), 0) # merge draft changes back to original published dataset - self._merge_draft_changes(draft_cr['id']) + self._merge_draft_changes(draft_cr["id"]) # ensure original now has the files - self.assertEqual(CR.objects.get(pk=cr['id']).files.count(), 1) + self.assertEqual(CR.objects.get(pk=cr["id"]).files.count(), 1) def test_add_files_to_draft_when_files_already_exist(self): """ @@ -612,11 +669,13 @@ def test_add_files_to_draft_when_files_already_exist(self): or removing files should be prevented. """ cr = self._create_dataset(with_files=True) - draft_cr = self._create_draft(cr['id']) + draft_cr = self._create_draft(cr["id"]) # add file to draft - file_changes = { 'files': [{ 'identifier': 'pid:urn:10' }]} - response = self.client.post('/rest/v2/datasets/%d/files' % draft_cr['id'], file_changes, format="json") + file_changes = {"files": [{"identifier": "pid:urn:10"}]} + response = self.client.post( + "/rest/v2/datasets/%d/files" % draft_cr["id"], file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_add_files_to_draft_cumulative_dataset(self): @@ -624,45 +683,49 @@ def test_add_files_to_draft_cumulative_dataset(self): Adding new files to cumulative draft should be ok. Removing files should be prevented. """ cr = self._create_dataset(cumulative=True, with_files=True) - draft_cr = self._create_draft(cr['id']) + draft_cr = self._create_draft(cr["id"]) # try to remove a file. should be stopped - file_changes = { 'files': [{ 'identifier': 'pid:urn:1', 'exclude': True }]} - response = self.client.post('/rest/v2/datasets/%d/files' % draft_cr['id'], file_changes, format="json") + file_changes = {"files": [{"identifier": "pid:urn:1", "exclude": True}]} + response = self.client.post( + "/rest/v2/datasets/%d/files" % draft_cr["id"], file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # now add files - file_changes = { 'files': [{ 'identifier': 'pid:urn:10' }]} - response = self.client.post('/rest/v2/datasets/%d/files' % draft_cr['id'], file_changes, format="json") + file_changes = {"files": [{"identifier": "pid:urn:10"}]} + response = self.client.post( + "/rest/v2/datasets/%d/files" % draft_cr["id"], file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # ensure original has no files YET - self.assertEqual(CR.objects.get(pk=cr['id']).files.count(), 2) + self.assertEqual(CR.objects.get(pk=cr["id"]).files.count(), 2) # merge draft changes back to original published dataset - self._merge_draft_changes(draft_cr['id']) + self._merge_draft_changes(draft_cr["id"]) # ensure original now has the files - self.assertEqual(CR.objects.get(pk=cr['id']).files.count(), 3) + self.assertEqual(CR.objects.get(pk=cr["id"]).files.count(), 3) def test_delete_draft_of_published_dataset(self): """ Delete draft of a published dataset. """ cr = self._create_dataset(with_files=False) - draft_cr = self._create_draft(cr['id']) + draft_cr = self._create_draft(cr["id"]) - response = self.client.delete('/rest/v2/datasets/%d' % draft_cr['id'], format="json") + response = self.client.delete("/rest/v2/datasets/%d" % draft_cr["id"], format="json") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) # draft should be deleted permanently - draft_found = CR.objects_unfiltered.filter(pk=draft_cr['id']).exists() + draft_found = CR.objects_unfiltered.filter(pk=draft_cr["id"]).exists() self.assertEqual(draft_found, False) # ensure original now has a link to next_draft - response = self.client.get('/rest/v2/datasets/%s' % cr['id'], format="json") + response = self.client.get("/rest/v2/datasets/%s" % cr["id"], format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('next_draft' in response.data, False, 'next_draft link should be gone') + self.assertEqual("next_draft" in response.data, False, "next_draft link should be gone") def test_deprecated_draft(self): """ @@ -670,37 +733,47 @@ def test_deprecated_draft(self): """ cr = self._create_dataset(with_files=True) - cr_files = self.client.get('/rest/v2/datasets/%s?include_user_metadata&file_details' - % cr['id'], format="json") - cr_files = [f['identifier'] for f in cr_files.data['research_dataset']['files']] + cr_files = self.client.get( + "/rest/v2/datasets/%s?include_user_metadata&file_details" % cr["id"], + format="json", + ) + cr_files = [f["identifier"] for f in cr_files.data["research_dataset"]["files"]] - draft_cr = self.client.post('/rpc/v2/datasets/create_draft?identifier=%d' % cr['id'], format="json") + draft_cr = self.client.post( + "/rpc/v2/datasets/create_draft?identifier=%d" % cr["id"], format="json" + ) - delete_files = self.client.delete('/rest/v2/files', cr_files, format="json") + delete_files = self.client.delete("/rest/v2/files", cr_files, format="json") self.assertEqual(delete_files.status_code, status.HTTP_200_OK, delete_files.data) - deprecated = self.client.get('/rest/v2/datasets/%s' % cr['id'], format='json') + deprecated = self.client.get("/rest/v2/datasets/%s" % cr["id"], format="json") self.assertEqual(deprecated.status_code, status.HTTP_200_OK, deprecated.data) - self.assertTrue(deprecated.data['deprecated'], deprecated.data['deprecated']) + self.assertTrue(deprecated.data["deprecated"], deprecated.data["deprecated"]) - response = self.client.post('/rpc/v2/datasets/merge_draft?identifier=%d' % draft_cr.data['id'], format="json") + response = self.client.post( + "/rpc/v2/datasets/merge_draft?identifier=%d" % draft_cr.data["id"], + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('The origin dataset of this draft is deprecated' in response.data['detail'][0], response.data) + self.assertTrue( + "The origin dataset of this draft is deprecated" in response.data["detail"][0], + response.data, + ) def test_delete_published_dataset_with_an_unmerged_draft(self): """ Delete published dataset that has an unmerged draft """ cr = self._create_dataset(with_files=False) - draft_cr = self._create_draft(cr['id']) + draft_cr = self._create_draft(cr["id"]) - response = self.client.delete('/rest/v2/datasets/%d' % cr['id'], format="json") + response = self.client.delete("/rest/v2/datasets/%d" % cr["id"], format="json") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) # Both are be deleted - draft_found = CR.objects_unfiltered.filter(pk=draft_cr['id']).exists() + draft_found = CR.objects_unfiltered.filter(pk=draft_cr["id"]).exists() self.assertEqual(draft_found, False) - original = self.client.get('/rest/v2/datasets/%s?removed=true' % cr['id'], format='json') + original = self.client.get("/rest/v2/datasets/%s?removed=true" % cr["id"], format="json") self.assertEqual(original.status_code, status.HTTP_200_OK, original.data) - self.assertEqual(original.data['removed'], True, original.data) + self.assertEqual(original.data["removed"], True, original.data) diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/filehandling.py b/src/metax_api/tests/api/rest/v2/views/datasets/filehandling.py index 86e20140..e1b49f43 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/filehandling.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/filehandling.py @@ -24,19 +24,21 @@ class CatalogRecordApiWriteAssignFilesCommon(CatalogRecordApiWriteCommon, TestCl """ def _get_file_from_test_data(self): - from_test_data = self._get_object_from_test_data('file', requested_index=0) - from_test_data.update({ - "checksum": { - "value": "checksumvalue", - "algorithm": "SHA-256", - "checked": "2017-05-23T10:07:22.559656Z", - }, - "file_name": "must_replace", - "file_path": "must_replace", - "identifier": "must_replace", - "project_identifier": "must_replace", - "file_storage": self._get_object_from_test_data('filestorage', requested_index=0) - }) + from_test_data = self._get_object_from_test_data("file", requested_index=0) + from_test_data.update( + { + "checksum": { + "value": "checksumvalue", + "algorithm": "SHA-256", + "checked": "2017-05-23T10:07:22.559656Z", + }, + "file_name": "must_replace", + "file_path": "must_replace", + "identifier": "must_replace", + "project_identifier": "must_replace", + "file_storage": self._get_object_from_test_data("filestorage", requested_index=0), + } + ) return from_test_data def _form_test_file_hierarchy(self): @@ -48,72 +50,72 @@ def _form_test_file_hierarchy(self): { "file_name": "file_01.txt", "file_path": "/TestExperiment/Directory_1/Group_1/file_01.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_02.txt", "file_path": "/TestExperiment/Directory_1/Group_1/file_02.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_03.txt", "file_path": "/TestExperiment/Directory_1/Group_2/file_03.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_04.txt", "file_path": "/TestExperiment/Directory_1/Group_2/file_04.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_05.txt", "file_path": "/TestExperiment/Directory_1/file_05.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_06.txt", "file_path": "/TestExperiment/Directory_1/file_06.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_07.txt", "file_path": "/TestExperiment/Directory_2/Group_1/file_07.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_08.txt", "file_path": "/TestExperiment/Directory_2/Group_1/file_08.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_09.txt", "file_path": "/TestExperiment/Directory_2/Group_2/file_09.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_10.txt", "file_path": "/TestExperiment/Directory_2/Group_2/file_10.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_11.txt", "file_path": "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_12.txt", "file_path": "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_12.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_13.txt", "file_path": "/TestExperiment/Directory_2/file_13.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_14.txt", "file_path": "/TestExperiment/Directory_2/file_14.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, ] @@ -121,70 +123,69 @@ def _form_test_file_hierarchy(self): { "file_name": "file_15.txt", "file_path": "/SecondExperiment/Directory_1/Group_1/file_15.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_16.txt", "file_path": "/SecondExperiment/Directory_1/Group_1/file_16.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_17.txt", "file_path": "/SecondExperiment/Directory_1/Group_2/file_18.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_18.txt", "file_path": "/SecondExperiment/Directory_1/Group_2/file_18.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_19.txt", "file_path": "/SecondExperiment/Directory_1/file_19.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_20.txt", "file_path": "/SecondExperiment/Directory_1/file_20.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_21.txt", "file_path": "/SecondExperiment/Data/file_21.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_22.txt", "file_path": "/SecondExperiment/Data_Config/file_22.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_23.txt", "file_path": "/SecondExperiment/Data_Config/file_23.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, { "file_name": "file_24.txt", "file_path": "/SecondExperiment/Data/History/file_24.txt", - 'project_identifier': 'testproject_2', + "project_identifier": "testproject_2", }, - ] file_template = self._get_file_from_test_data() - del file_template['id'] - self._single_file_byte_size = file_template['byte_size'] + del file_template["id"] + self._single_file_byte_size = file_template["byte_size"] files_1 = [] for i, f in enumerate(file_data_1): file = deepcopy(file_template) - file.update(f, identifier='test:file:%s' % f['file_name'][-6:-4]) + file.update(f, identifier="test:file:%s" % f["file_name"][-6:-4]) files_1.append(file) files_2 = [] for i, f in enumerate(file_data_2): file = deepcopy(file_template) - file.update(f, identifier='test:file:%s' % f['file_name'][-6:-4]) + file.update(f, identifier="test:file:%s" % f["file_name"][-6:-4]) files_2.append(file) return files_1, files_2 @@ -196,8 +197,8 @@ def _research_dataset_or_file_changes(self, rd_or_file_changes): Methods who call this helper can treat the returned object as either, and only operate with "files" and "directories" keys. """ - if 'research_dataset' in rd_or_file_changes: - files_and_dirs = rd_or_file_changes['research_dataset'] + if "research_dataset" in rd_or_file_changes: + files_and_dirs = rd_or_file_changes["research_dataset"] else: files_and_dirs = rd_or_file_changes @@ -207,25 +208,25 @@ def _add_directory(self, ds, path, project=None): """ Add directory to research_dataset object or file_changes object. """ - params = { 'directory_path': path } + params = {"directory_path": path} if project: - params['project_identifier'] = project + params["project_identifier"] = project identifier = Directory.objects.get(**params).identifier files_and_dirs = self._research_dataset_or_file_changes(ds) - if 'directories' not in files_and_dirs: - files_and_dirs['directories'] = [] + if "directories" not in files_and_dirs: + files_and_dirs["directories"] = [] - files_and_dirs['directories'].append({ - "identifier": identifier, - "title": "Directory Title", - "description": "This is directory at %s" % path, - "use_category": { - "identifier": "method" + files_and_dirs["directories"].append( + { + "identifier": identifier, + "title": "Directory Title", + "description": "This is directory at %s" % path, + "use_category": {"identifier": "method"}, } - }) + ) def _add_file(self, ds, path): """ @@ -235,17 +236,17 @@ def _add_file(self, ds, path): files_and_dirs = self._research_dataset_or_file_changes(ds) - if 'files' not in files_and_dirs: - files_and_dirs['files'] = [] + if "files" not in files_and_dirs: + files_and_dirs["files"] = [] - files_and_dirs['files'].append({ - "identifier": identifier, - "title": "File Title", - "description": "This is file at %s" % path, - "use_category": { - "identifier": "method" + files_and_dirs["files"].append( + { + "identifier": identifier, + "title": "File Title", + "description": "This is file at %s" % path, + "use_category": {"identifier": "method"}, } - }) + ) def _add_nonexisting_directory(self, ds): """ @@ -253,14 +254,14 @@ def _add_nonexisting_directory(self, ds): """ files_and_dirs = self._research_dataset_or_file_changes(ds) - files_and_dirs['directories'] = [{ - "identifier": "doesnotexist", - "title": "Directory Title", - "description": "This is directory does not exist", - "use_category": { - "identifier": "method" + files_and_dirs["directories"] = [ + { + "identifier": "doesnotexist", + "title": "Directory Title", + "description": "This is directory does not exist", + "use_category": {"identifier": "method"}, } - }] + ] def _add_nonexisting_file(self, ds): """ @@ -268,14 +269,14 @@ def _add_nonexisting_file(self, ds): """ files_and_dirs = self._research_dataset_or_file_changes(ds) - files_and_dirs['files'] = [{ - "identifier": "doesnotexist", - "title": "File Title", - "description": "This is file does not exist", - "use_category": { - "identifier": "method" + files_and_dirs["files"] = [ + { + "identifier": "doesnotexist", + "title": "File Title", + "description": "This is file does not exist", + "use_category": {"identifier": "method"}, } - }] + ] def _remove_directory(self, ds, path): """ @@ -283,16 +284,16 @@ def _remove_directory(self, ds, path): """ files_and_dirs = self._research_dataset_or_file_changes(ds) - if 'directories' not in files_and_dirs: - raise Exception('ds has no dirs') + if "directories" not in files_and_dirs: + raise Exception("ds has no dirs") identifier = Directory.objects.get(directory_path=path).identifier - for i, dr in enumerate(files_and_dirs['directories']): - if dr['identifier'] == identifier: - ds['research_dataset']['directories'].pop(i) + for i, dr in enumerate(files_and_dirs["directories"]): + if dr["identifier"] == identifier: + ds["research_dataset"]["directories"].pop(i) return - raise Exception('path %s not found in directories' % path) + raise Exception("path %s not found in directories" % path) def _remove_file(self, ds, path): """ @@ -300,41 +301,41 @@ def _remove_file(self, ds, path): """ files_and_dirs = self._research_dataset_or_file_changes(ds) - if 'files' not in files_and_dirs: - raise Exception('ds has no files') + if "files" not in files_and_dirs: + raise Exception("ds has no files") identifier = File.objects.get(file_path=path).identifier - for i, f in enumerate(files_and_dirs['files']): - if f['identifier'] == identifier: - ds['research_dataset']['files'].pop(i) + for i, f in enumerate(files_and_dirs["files"]): + if f["identifier"] == identifier: + ds["research_dataset"]["files"].pop(i) return - raise Exception('path %s not found in files' % path) + raise Exception("path %s not found in files" % path) def _freeze_new_files(self): file_data = [ { "file_name": "file_90.txt", "file_path": "/TestExperiment/Directory_2/Group_3/file_90.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_91.txt", "file_path": "/TestExperiment/Directory_2/Group_3/file_91.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, ] file_template = self._get_file_from_test_data() - del file_template['id'] - self._single_file_byte_size = file_template['byte_size'] + del file_template["id"] + self._single_file_byte_size = file_template["byte_size"] files = [] for i, f in enumerate(file_data): file = deepcopy(file_template) - file.update(f, identifier='frozen:later:file:%s' % f['file_name'][-6:-4]) + file.update(f, identifier="frozen:later:file:%s" % f["file_name"][-6:-4]) files.append(file) - response = self.client.post('/rest/v2/files', files, format="json") + response = self.client.post("/rest/v2/files", files, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) def _freeze_files_to_root(self): @@ -342,25 +343,25 @@ def _freeze_files_to_root(self): { "file_name": "file_56.txt", "file_path": "/TestExperiment/Directory_2/file_56.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, { "file_name": "file_57.txt", "file_path": "/TestExperiment/Directory_2/file_57.txt", - 'project_identifier': 'testproject', + "project_identifier": "testproject", }, ] file_template = self._get_file_from_test_data() - del file_template['id'] - self._single_file_byte_size = file_template['byte_size'] + del file_template["id"] + self._single_file_byte_size = file_template["byte_size"] files = [] for i, f in enumerate(file_data): file = deepcopy(file_template) - file.update(f, identifier='frozen:later:file:%s' % f['file_name'][-6:-4]) + file.update(f, identifier="frozen:later:file:%s" % f["file_name"][-6:-4]) files.append(file) - response = self.client.post('/rest/v2/files', files, format="json") + response = self.client.post("/rest/v2/files", files, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) def setUp(self): @@ -370,30 +371,37 @@ def setUp(self): - create 12 new files in a new project """ super().setUp() - self.cr_test_data['research_dataset'].pop('id', None) - self.cr_test_data['research_dataset'].pop('preferred_identifier', None) - self.cr_test_data['research_dataset'].pop('files', None) - self.cr_test_data['research_dataset'].pop('directories', None) + self.cr_test_data["research_dataset"].pop("id", None) + self.cr_test_data["research_dataset"].pop("preferred_identifier", None) + self.cr_test_data["research_dataset"].pop("files", None) + self.cr_test_data["research_dataset"].pop("directories", None) project_files = self._form_test_file_hierarchy() for p_files in project_files: - response = self.client.post('/rest/v2/files', p_files, format="json") + response = self.client.post("/rest/v2/files", p_files, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def assert_preferred_identifier_changed(self, response, true_or_false): self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('next_dataset_version' in response.data, true_or_false, - 'this field should only be present if preferred_identifier changed') + self.assertEqual( + "next_dataset_version" in response.data, + true_or_false, + "this field should only be present if preferred_identifier changed", + ) if true_or_false is True: - self.assertEqual(response.data['research_dataset']['preferred_identifier'] != - response.data['next_dataset_version']['preferred_identifier'], true_or_false) + self.assertEqual( + response.data["research_dataset"]["preferred_identifier"] + != response.data["next_dataset_version"]["preferred_identifier"], + true_or_false, + ) def assert_file_count(self, cr, expected_file_count): self.assertEqual( - CatalogRecordV2.objects.get(pk=cr if type(cr) is int else cr['id']).files.count(), expected_file_count + CatalogRecordV2.objects.get(pk=cr if type(cr) is int else cr["id"]).files.count(), + expected_file_count, ) def assert_total_files_byte_size(self, cr, expected_size): - self.assertEqual(cr['research_dataset']['total_files_byte_size'], expected_size) + self.assertEqual(cr["research_dataset"]["total_files_byte_size"], expected_size) class CatalogRecordApiWriteAssignFilesCommonV2(CatalogRecordApiWriteAssignFilesCommon): @@ -415,16 +423,16 @@ def _add_file(self, ds, path, with_metadata=False): # else: addition entry only, which will not be persisted. keep only identifier - files_and_dirs['files'][-1] = { 'identifier': files_and_dirs['files'][-1]['identifier'] } + files_and_dirs["files"][-1] = {"identifier": files_and_dirs["files"][-1]["identifier"]} def _exclude_file(self, ds, path): self._add_file(ds, path) files_and_dirs = self._research_dataset_or_file_changes(ds) - files_and_dirs['files'][-1]['exclude'] = True + files_and_dirs["files"][-1]["exclude"] = True - assert len(files_and_dirs['files'][-1]) == 2 + assert len(files_and_dirs["files"][-1]) == 2 def _add_directory(self, ds, path, project=None, with_metadata=False): super()._add_directory(ds, path) @@ -436,33 +444,34 @@ def _add_directory(self, ds, path, project=None, with_metadata=False): # else: addition entry only, which will not be persisted. keep only identifier - files_and_dirs['directories'][-1] = { 'identifier': files_and_dirs['directories'][-1]['identifier'] } + files_and_dirs["directories"][-1] = { + "identifier": files_and_dirs["directories"][-1]["identifier"] + } def _exclude_directory(self, ds, path): self._add_directory(ds, path) files_and_dirs = self._research_dataset_or_file_changes(ds) - files_and_dirs['directories'][-1]['exclude'] = True + files_and_dirs["directories"][-1]["exclude"] = True - assert len(files_and_dirs['directories'][-1]) == 2 + assert len(files_and_dirs["directories"][-1]) == 2 class CatalogRecordFileHandling(CatalogRecordApiWriteAssignFilesCommonV2): - def _set_token_authentication(self): self.create_end_user_data_catalogs() self.token = get_test_oidc_token() - self.token['group_names'].append('IDA01:testproject') - self._use_http_authorization(method='bearer', token=self.token) + self.token["group_names"].append("IDA01:testproject") + self._use_http_authorization(method="bearer", token=self.token) self._mock_token_validation_succeeds() def _create_draft(self): - self.cr_test_data['research_dataset'].pop('files', None) - self.cr_test_data['research_dataset'].pop('directories', None) - response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"].pop("files", None) + self.cr_test_data["research_dataset"].pop("directories", None) + response = self.client.post("/rest/v2/datasets?draft", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - return response.data['id'] + return response.data["id"] @responses.activate def test_authorization(self): @@ -474,9 +483,13 @@ def test_authorization(self): self._set_token_authentication() for with_metadata in (False, True): - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_05.txt') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_06.txt', with_metadata=with_metadata) - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_05.txt") + self._add_file( + self.cr_test_data, + "/TestExperiment/Directory_1/file_06.txt", + with_metadata=with_metadata, + ) + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) @responses.activate @@ -487,24 +500,24 @@ def test_retrieve_dataset_file_projects(self): """ self._set_token_authentication() - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_05.txt') - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_05.txt") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] + cr_id = response.data["id"] # user owns dataset - response = self.client.get('/rest/v2/datasets/%d/projects' % cr_id, format="json") + response = self.client.get("/rest/v2/datasets/%d/projects" % cr_id, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), 1, response.data) # user is authenticated, but does not own dataset - response = self.client.get('/rest/v2/datasets/1/projects', format="json") + response = self.client.get("/rest/v2/datasets/1/projects", format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) # anonymous user self.client._credentials = {} - response = self.client.get('/rest/v2/datasets/%d/projects' % cr_id, format="json") + response = self.client.get("/rest/v2/datasets/%d/projects" % cr_id, format="json") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) def test_include_user_metadata_parameter(self): @@ -512,24 +525,30 @@ def test_include_user_metadata_parameter(self): When retrieving datasets, by default the "user metadata", or "dataset-specific file metadata" stored in research_dataset.files and research_dataset.directories should not be returned. """ - response = self.client.get('/rest/v2/datasets/1', format="json") + response = self.client.get("/rest/v2/datasets/1", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('files' in response.data['research_dataset'], False, response.data) + self.assertEqual("files" in response.data["research_dataset"], False, response.data) - response = self.client.get('/rest/v2/datasets/1?include_user_metadata', format="json") + response = self.client.get("/rest/v2/datasets/1?include_user_metadata", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('files' in response.data['research_dataset'], True, response.data) + self.assertEqual("files" in response.data["research_dataset"], True, response.data) def test_create_files_are_saved(self): """ A very simple "add two individual files" test. Only entries with dataset-specific metadata should be persisted in research_dataset.files. """ - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_05.txt') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_06.txt', with_metadata=True) - response = self.client.post('/rest/v2/datasets?include_user_metadata', self.cr_test_data, format="json") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_05.txt") + self._add_file( + self.cr_test_data, + "/TestExperiment/Directory_1/file_06.txt", + with_metadata=True, + ) + response = self.client.post( + "/rest/v2/datasets?include_user_metadata", self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(len(response.data['research_dataset']['files']), 1) + self.assertEqual(len(response.data["research_dataset"]["files"]), 1) self.assert_file_count(response.data, 2) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 2) @@ -537,12 +556,20 @@ def test_allowed_projects(self): """ Test the ?allowed_projects=x,y parameter when adding files. """ - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_05.txt') + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_05.txt") - response = self.client.post('/rest/v2/datasets?allowed_projects=testproject', self.cr_test_data, format="json") + response = self.client.post( + "/rest/v2/datasets?allowed_projects=testproject", + self.cr_test_data, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - response = self.client.post('/rest/v2/datasets?allowed_projects=no,projects', self.cr_test_data, format="json") + response = self.client.post( + "/rest/v2/datasets?allowed_projects=no,projects", + self.cr_test_data, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) def test_create_directories_are_saved(self): @@ -550,11 +577,15 @@ def test_create_directories_are_saved(self): A very simple "add two individual directories" test. Only entries with dataset-specific metadata should be persisted in research_dataset.directories. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_1') - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_2', with_metadata=True) - response = self.client.post('/rest/v2/datasets?include_user_metadata', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1/Group_1") + self._add_directory( + self.cr_test_data, "/TestExperiment/Directory_1/Group_2", with_metadata=True + ) + response = self.client.post( + "/rest/v2/datasets?include_user_metadata", self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(len(response.data['research_dataset']['directories']), 1) + self.assertEqual(len(response.data["research_dataset"]["directories"]), 1) self.assert_file_count(response.data, 4) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 4) @@ -562,9 +593,9 @@ def test_create_exclude_files(self): """ Add directory of files, but exclude one file. """ - self._exclude_file(self.cr_test_data, '/TestExperiment/Directory_1/Group_1/file_01.txt') - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1') - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + self._exclude_file(self.cr_test_data, "/TestExperiment/Directory_1/Group_1/file_01.txt") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 5) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 5) @@ -573,9 +604,9 @@ def test_create_exclude_directories(self): """ Add directory of files, but exclude one sub directory. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1') - self._exclude_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_1') - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1") + self._exclude_directory(self.cr_test_data, "/TestExperiment/Directory_1/Group_1") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 4) self.assert_total_files_byte_size(response.data, self._single_file_byte_size * 4) @@ -590,54 +621,66 @@ def test_update_add_and_exclude_files(self): file_changes = {} - self._add_file(file_changes, '/TestExperiment/Directory_1/Group_1/file_01.txt') - response = self.client.post('/rest/v2/datasets/%d/files' % cr_id, file_changes, format="json") + self._add_file(file_changes, "/TestExperiment/Directory_1/Group_1/file_01.txt") + response = self.client.post( + "/rest/v2/datasets/%d/files" % cr_id, file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('files_added'), 1, response.data) + self.assertEqual(response.data.get("files_added"), 1, response.data) self.assert_file_count(cr_id, 1) # executing the same request with same file entry should make no difference - response = self.client.post('/rest/v2/datasets/%d/files' % cr_id, file_changes, format="json") + response = self.client.post( + "/rest/v2/datasets/%d/files" % cr_id, file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('files_added'), 0, response.data) + self.assertEqual(response.data.get("files_added"), 0, response.data) self.assert_file_count(cr_id, 1) # adding a directory should add files that are not already added file_changes = {} - self._add_directory(file_changes, '/TestExperiment/Directory_1/Group_1') - response = self.client.post('/rest/v2/datasets/%d/files' % cr_id, file_changes, format="json") + self._add_directory(file_changes, "/TestExperiment/Directory_1/Group_1") + response = self.client.post( + "/rest/v2/datasets/%d/files" % cr_id, file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('files_added'), 1, response.data) + self.assertEqual(response.data.get("files_added"), 1, response.data) self.assert_file_count(cr_id, 2) # add even more files file_changes = {} - self._add_directory(file_changes, '/TestExperiment/Directory_1') - response = self.client.post('/rest/v2/datasets/%d/files' % cr_id, file_changes, format="json") + self._add_directory(file_changes, "/TestExperiment/Directory_1") + response = self.client.post( + "/rest/v2/datasets/%d/files" % cr_id, file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('files_added'), 4, response.data) + self.assertEqual(response.data.get("files_added"), 4, response.data) self.assert_file_count(cr_id, 6) # exclude previously added files from one directroy file_changes = {} - self._exclude_directory(file_changes, '/TestExperiment/Directory_1/Group_1') - response = self.client.post('/rest/v2/datasets/%d/files' % cr_id, file_changes, format="json") + self._exclude_directory(file_changes, "/TestExperiment/Directory_1/Group_1") + response = self.client.post( + "/rest/v2/datasets/%d/files" % cr_id, file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('files_removed'), 2, response.data) + self.assertEqual(response.data.get("files_removed"), 2, response.data) self.assert_file_count(cr_id, 4) # exclude all previously added files, but keep one file by adding an "add file" entry file_changes = {} - self._add_file(file_changes, '/TestExperiment/Directory_1/Group_2/file_03.txt') - self._exclude_directory(file_changes, '/TestExperiment') - response = self.client.post('/rest/v2/datasets/%d/files' % cr_id, file_changes, format="json") + self._add_file(file_changes, "/TestExperiment/Directory_1/Group_2/file_03.txt") + self._exclude_directory(file_changes, "/TestExperiment") + response = self.client.post( + "/rest/v2/datasets/%d/files" % cr_id, file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('files_added'), 0, response.data) - self.assertEqual(response.data.get('files_removed'), 3, response.data) + self.assertEqual(response.data.get("files_added"), 0, response.data) + self.assertEqual(response.data.get("files_removed"), 3, response.data) self.assert_file_count(cr_id, 1) def test_files_can_be_added_once_after_publishing(self): @@ -646,111 +689,161 @@ def test_files_can_be_added_once_after_publishing(self): without needing to create new dataset versions, so this is permitted. Subsequent file changes will requiree creating a new draft version first. """ - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] + cr_id = response.data["id"] self.assert_file_count(cr_id, 0) file_changes = {} - self._add_file(file_changes, '/TestExperiment/Directory_1/Group_1/file_02.txt') - response = self.client.post('/rest/v2/datasets/%d/files' % cr_id, file_changes, format="json") + self._add_file(file_changes, "/TestExperiment/Directory_1/Group_1/file_02.txt") + response = self.client.post( + "/rest/v2/datasets/%d/files" % cr_id, file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('files_added'), 1, response.data) + self.assertEqual(response.data.get("files_added"), 1, response.data) # try to add a second time. should give an error file_changes = {} - self._add_file(file_changes, '/TestExperiment/Directory_1/Group_1/file_02.txt') - response = self.client.post('/rest/v2/datasets/%d/files' % cr_id, file_changes, format="json") + self._add_file(file_changes, "/TestExperiment/Directory_1/Group_1/file_02.txt") + response = self.client.post( + "/rest/v2/datasets/%d/files" % cr_id, file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('Changing files of a published dataset' in response.data['detail'][0], True, response.data) + self.assertEqual( + "Changing files of a published dataset" in response.data["detail"][0], + True, + response.data, + ) def test_prevent_changing_files_on_deprecated_datasets(self): cr = CR.objects.get(pk=1) cr.deprecated = True cr.force_save() - file_changes = { - 'files': [{ 'identifier': 'some file' }] - } + file_changes = {"files": [{"identifier": "some file"}]} - response = self.client.post('/rest/v2/datasets/1/files', file_changes, format="json") + response = self.client.post("/rest/v2/datasets/1/files", file_changes, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('Changing files of a deprecated' in response.data['detail'][0], True, response.data) + self.assertEqual( + "Changing files of a deprecated" in response.data["detail"][0], + True, + response.data, + ) def test_prevent_adding_files_with_normal_update(self): cr_id = self._create_draft() - cr = self.client.get(f'/rest/v2/datasets/{cr_id}', format="json").data + cr = self.client.get(f"/rest/v2/datasets/{cr_id}", format="json").data - for type in ['files', 'directories']: - cr['research_dataset'][type] = [ - { - "identifier": 'pid:urn:%s1' % '' if type == 'files' else 'dir:' - } + for type in ["files", "directories"]: + cr["research_dataset"][type] = [ + {"identifier": "pid:urn:%s1" % "" if type == "files" else "dir:"} ] - response = self.client.put(f'/rest/v2/datasets/{cr_id}?include_user_metadata', cr, format="json") + response = self.client.put( + f"/rest/v2/datasets/{cr_id}?include_user_metadata", cr, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(type in response.data['research_dataset'], False, response.data) + self.assertEqual(type in response.data["research_dataset"], False, response.data) - cr['research_dataset'].pop(type) + cr["research_dataset"].pop(type) # test for published dataset - self.cr_test_data.pop('files', None) - self.cr_test_data.pop('directories', None) + self.cr_test_data.pop("files", None) + self.cr_test_data.pop("directories", None) - response = self.client.post('/rest/v2/datasets/', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets/", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr = response.data - for type in ['files', 'directories']: - cr['research_dataset'][type] = [ - { - "identifier": 'pid:urn:%s1' % '' if type == 'files' else 'dir:' - } + for type in ["files", "directories"]: + cr["research_dataset"][type] = [ + {"identifier": "pid:urn:%s1" % "" if type == "files" else "dir:"} ] - response = self.client.put(f'/rest/v2/datasets/{cr["id"]}?include_user_metadata', cr, format="json") + response = self.client.put( + f'/rest/v2/datasets/{cr["id"]}?include_user_metadata', cr, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(type in response.data['research_dataset'], False, response.data) + self.assertEqual(type in response.data["research_dataset"], False, response.data) - cr['research_dataset'].pop(type) + cr["research_dataset"].pop(type) def test_directory_entries_are_processed_in_order(self): """ Directory entries should executed in the order they are given in the request body. """ # excluding should do nothing, since "add directory" entry is later - self._exclude_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_1') - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1') - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + self._exclude_directory(self.cr_test_data, "/TestExperiment/Directory_1/Group_1") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 6) - self.cr_test_data['research_dataset'].pop('directories') + self.cr_test_data["research_dataset"].pop("directories") # exclusion should now have effect, since it is last - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_1') - self._exclude_directory(self.cr_test_data, '/TestExperiment/Directory_1/Group_1') - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_1") + self._exclude_directory(self.cr_test_data, "/TestExperiment/Directory_1/Group_1") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) self.assert_file_count(response.data, 4) def test_allow_file_changes_only_on_drafts(self): - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/Group_1/file_01.txt') - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/Group_1/file_01.txt") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) file_changes = {} - self._add_file(file_changes, '/TestExperiment/Directory_1/Group_1/file_02.txt') - response = self.client.post('/rest/v2/datasets/%d/files' % response.data['id'], file_changes, format="json") + self._add_file(file_changes, "/TestExperiment/Directory_1/Group_1/file_02.txt") + response = self.client.post( + "/rest/v2/datasets/%d/files" % response.data["id"], + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('Changing files of a published dataset' in response.data['detail'][0], True, response.data) + self.assertEqual( + "Changing files of a published dataset" in response.data["detail"][0], + True, + response.data, + ) + + def test_total_files_byte_size_is_updated_after_adding_files(self): + # create a draft dataset with zero files + cr_id = self._create_draft() + response = self.client.get(f"/rest/v2/datasets/{cr_id}") + cr_id = response.data["id"] + self.assertEqual(response.data.get("research_dataset").get("total_files_byte_size"), None) + + # add file to dataset + file_changes = {} + + self._add_file(file_changes, "/TestExperiment/Directory_1/Group_1/file_01.txt") + response = self.client.post( + f"/rest/v2/datasets/{cr_id}/files", file_changes, format="json" + ) + self.assertEqual(response.data.get("files_added"), 1, response.data) + self.assert_file_count(cr_id, 1) + response = self.client.get(f"/rest/v2/datasets/{cr_id}") + self.assert_total_files_byte_size(response.data, 100) + + def test_total_files_byte_size_field_is_dropped_from_drafts_with_no_files(self): + # draft with no files/dirs does not have total_files_byte_size field + cr_id = self._create_draft() + response = self.client.get(f"/rest/v2/datasets/{cr_id}") + self.assertEqual(response.data.get("research_dataset").get("total_files_byte_size"), None) + + def test_total_files_byte_size_field_is_dropped_from_datasets_with_no_files(self): + # dataset with no files/dirs does not have total_files_byte_size field + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") + cr_id = response.data["id"] + response = self.client.get(f"/rest/v2/datasets/{cr_id}") + self.assertEqual(response.data.get("research_dataset").get("total_files_byte_size"), None) class CatalogRecordUserMetadata(CatalogRecordApiWriteAssignFilesCommonV2): @@ -764,16 +857,18 @@ def test_retrieve_file_metadata_only(self): cr_id = 11 # retrieve all "user metadata" of adataset - response = self.client.get('/rest/v2/datasets/%d/files/user_metadata' % cr_id, format="json") + response = self.client.get( + "/rest/v2/datasets/%d/files/user_metadata" % cr_id, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - cr = CR.objects.only('research_dataset').get(pk=cr_id) + cr = CR.objects.only("research_dataset").get(pk=cr_id) - for object_type in ('files', 'directories'): + for object_type in ("files", "directories"): self.assertEqual( len(cr.research_dataset[object_type]), len(response.data[object_type]), - response.data + response.data, ) files_and_dirs = response.data @@ -781,234 +876,307 @@ def test_retrieve_file_metadata_only(self): params = [ { # a pid of single file - 'pid': 'pid:urn:2', - 'directory': 'false', - 'expect_to_find': True, + "pid": "pid:urn:2", + "directory": "false", + "expect_to_find": True, }, { # a pid of a directory - 'pid': 'pid:urn:dir:2', - 'directory': 'true', - 'expect_to_find': True, + "pid": "pid:urn:dir:2", + "directory": "true", + "expect_to_find": True, }, { # a pid of a directory not part of this dataset - 'pid': 'should not be found', - 'directory': 'true', - 'expect_to_find': False, - } + "pid": "should not be found", + "directory": "true", + "expect_to_find": False, + }, ] for p in params: # retrieve a single metadata entry response = self.client.get( - '/rest/v2/datasets/%d/files/%s/user_metadata?directory=%s' % (cr_id, p['pid'], p['directory']), - format="json" + "/rest/v2/datasets/%d/files/%s/user_metadata?directory=%s" + % (cr_id, p["pid"], p["directory"]), + format="json", ) - if p['expect_to_find'] is True: + if p["expect_to_find"] is True: self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('identifier' in response.data, True, response.data) + self.assertEqual("identifier" in response.data, True, response.data) else: self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) continue - if p['directory'] == 'true': - object_type = 'directories' + if p["directory"] == "true": + object_type = "directories" else: - object_type = 'files' + object_type = "files" for obj in files_and_dirs[object_type]: - if obj['identifier'] == response.data['identifier']: - if p['expect_to_find'] is True: + if obj["identifier"] == response.data["identifier"]: + if p["expect_to_find"] is True: self.assertEqual(obj, response.data, response.data) break else: - self.fail('pid %s should not have been found' % p['pid']) + self.fail("pid %s should not have been found" % p["pid"]) else: - if p['expect_to_find'] is True: - self.fail('Retrieved object %s was not found in research_dataset file data?' % p['pid']) + if p["expect_to_find"] is True: + self.fail( + "Retrieved object %s was not found in research_dataset file data?" + % p["pid"] + ) def test_dataset_files_schema(self): """ Ensure new schema file dataset_files_schema.json is used. """ - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] + cr_id = response.data["id"] # use a non-schema field file_changes = {} - self._add_file(file_changes, '/TestExperiment/Directory_1/Group_1/file_02.txt', with_metadata=True) - file_changes['files'][0]['some_unexpected_file'] = 'should raise error' + self._add_file( + file_changes, + "/TestExperiment/Directory_1/Group_1/file_02.txt", + with_metadata=True, + ) + file_changes["files"][0]["some_unexpected_file"] = "should raise error" - response = self.client.post('/rest/v2/datasets/%d/files' % cr_id, file_changes, format="json") + response = self.client.post( + "/rest/v2/datasets/%d/files" % cr_id, file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('is not valid' in str(response.data['detail'][0]), True, response.data) + self.assertEqual("is not valid" in str(response.data["detail"][0]), True, response.data) # various mandatory fields missing - for mandatory_field in ('identifier', 'use_category'): + for mandatory_field in ("identifier", "use_category"): file_changes = {} - self._add_file(file_changes, '/TestExperiment/Directory_1/Group_1/file_02.txt', with_metadata=True) - file_changes['files'][0].pop(mandatory_field) + self._add_file( + file_changes, + "/TestExperiment/Directory_1/Group_1/file_02.txt", + with_metadata=True, + ) + file_changes["files"][0].pop(mandatory_field) - response = self.client.post('/rest/v2/datasets/%d/files' % cr_id, file_changes, format="json") + response = self.client.post( + "/rest/v2/datasets/%d/files" % cr_id, file_changes, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('is not valid' in str(response.data['detail'][0]), True, response.data) + self.assertEqual("is not valid" in str(response.data["detail"][0]), True, response.data) def test_update_metadata_only(self): - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/Group_1/file_02.txt') - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/Group_1/file_02.txt") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('files' in response.data['research_dataset'], False, response.data) + self.assertEqual("files" in response.data["research_dataset"], False, response.data) - cr_id = response.data['id'] + cr_id = response.data["id"] # add metadata for one file file_changes = {} - self._add_file(file_changes, '/TestExperiment/Directory_1/Group_1/file_02.txt', with_metadata=True) - file_changes['files'][0]['title'] = 'New title' + self._add_file( + file_changes, + "/TestExperiment/Directory_1/Group_1/file_02.txt", + with_metadata=True, + ) + file_changes["files"][0]["title"] = "New title" - response = self.client.put('/rest/v2/datasets/%d/files/user_metadata' % cr_id, file_changes, format="json") + response = self.client.put( + "/rest/v2/datasets/%d/files/user_metadata" % cr_id, + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual( - CR.objects.only('research_dataset').get(pk=cr_id).research_dataset['files'][0]['title'], - file_changes['files'][0]['title'] + CR.objects.only("research_dataset").get(pk=cr_id).research_dataset["files"][0]["title"], + file_changes["files"][0]["title"], ) # add metadata for two directories file_changes = {} - self._add_directory(file_changes, '/TestExperiment', with_metadata=True) - self._add_directory(file_changes, '/TestExperiment/Directory_1', with_metadata=True) - file_changes['directories'][0]['title'] = 'New dir title' - file_changes['directories'][0]['description'] = 'New dir description' - file_changes['directories'][1]['title'] = 'New dir title 2' - file_changes['directories'][1]['description'] = 'New dir description 2' + self._add_directory(file_changes, "/TestExperiment", with_metadata=True) + self._add_directory(file_changes, "/TestExperiment/Directory_1", with_metadata=True) + file_changes["directories"][0]["title"] = "New dir title" + file_changes["directories"][0]["description"] = "New dir description" + file_changes["directories"][1]["title"] = "New dir title 2" + file_changes["directories"][1]["description"] = "New dir description 2" file_count_before = CR.objects.get(pk=cr_id).files.count() - response = self.client.put('/rest/v2/datasets/%d/files/user_metadata' % cr_id, file_changes, format="json") + response = self.client.put( + "/rest/v2/datasets/%d/files/user_metadata" % cr_id, + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - cr = CR.objects.only('research_dataset').get(pk=cr_id) + cr = CR.objects.only("research_dataset").get(pk=cr_id) self.assertEqual( - cr.files.count(), file_count_before, 'operation should only update metadata, but not add files' + cr.files.count(), + file_count_before, + "operation should only update metadata, but not add files", ) for index in (0, 1): - for field in ('title', 'description'): + for field in ("title", "description"): self.assertEqual( - cr.research_dataset['directories'][index][field], - file_changes['directories'][index][field] + cr.research_dataset["directories"][index][field], + file_changes["directories"][index][field], ) # update only one field using patch file_changes = {} - self._add_directory(file_changes, '/TestExperiment', with_metadata=True) - file_changes['directories'][0] = { - 'identifier': file_changes['directories'][0]['identifier'], - 'title': 'Changed dir title' + self._add_directory(file_changes, "/TestExperiment", with_metadata=True) + file_changes["directories"][0] = { + "identifier": file_changes["directories"][0]["identifier"], + "title": "Changed dir title", } - response = self.client.patch('/rest/v2/datasets/%d/files/user_metadata' % cr_id, file_changes, format="json") + response = self.client.patch( + "/rest/v2/datasets/%d/files/user_metadata" % cr_id, + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual( - CR.objects.only('research_dataset').get(pk=cr_id).research_dataset['directories'][0]['title'], - file_changes['directories'][0]['title'] + CR.objects.only("research_dataset") + .get(pk=cr_id) + .research_dataset["directories"][0]["title"], + file_changes["directories"][0]["title"], ) # remove metadata entry. it should be ok that there are normal metadata-addition entries included # in the request body too. file_changes = {} - self._add_directory(file_changes, '/TestExperiment/Directory_1/Group_1', with_metadata=True) - self._add_directory(file_changes, '/TestExperiment') - file_changes['directories'][-1]['delete'] = True + self._add_directory(file_changes, "/TestExperiment/Directory_1/Group_1", with_metadata=True) + self._add_directory(file_changes, "/TestExperiment") + file_changes["directories"][-1]["delete"] = True - entry_to_delete = file_changes['directories'][-1]['identifier'] + entry_to_delete = file_changes["directories"][-1]["identifier"] - response = self.client.put('/rest/v2/datasets/%d/files/user_metadata' % cr_id, file_changes, format="json") + response = self.client.put( + "/rest/v2/datasets/%d/files/user_metadata" % cr_id, + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - cr = CR.objects.only('research_dataset').get(pk=cr_id) + cr = CR.objects.only("research_dataset").get(pk=cr_id) self.assertEqual( - entry_to_delete in [ dr['identifier'] for dr in cr.research_dataset['directories'] ], - False + entry_to_delete in [dr["identifier"] for dr in cr.research_dataset["directories"]], + False, ) # dont allow adding metadata entries for files that are not actually included in the dataset file_changes = {} - self._add_file(file_changes, '/TestExperiment/Directory_1/Group_2/file_03.txt', with_metadata=True) - non_existing_file = file_changes['files'][-1]['identifier'] + self._add_file( + file_changes, + "/TestExperiment/Directory_1/Group_2/file_03.txt", + with_metadata=True, + ) + non_existing_file = file_changes["files"][-1]["identifier"] - response = self.client.put('/rest/v2/datasets/%d/files/user_metadata' % cr_id, file_changes, format="json") + response = self.client.put( + "/rest/v2/datasets/%d/files/user_metadata" % cr_id, + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('are not included' in response.data['detail'][0], True, response.data) - self.assertEqual(non_existing_file in response.data['data'], True, response.data) + self.assertEqual("are not included" in response.data["detail"][0], True, response.data) + self.assertEqual(non_existing_file in response.data["data"], True, response.data) def test_delete_all_file_meta_data(self): # create dataset with file - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/Group_1/file_02.txt') - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/Group_1/file_02.txt") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('files' in response.data['research_dataset'], False, response.data) + self.assertEqual("files" in response.data["research_dataset"], False, response.data) - cr_id = response.data['id'] + cr_id = response.data["id"] # add metadata for one file file_changes = {} - self._add_file(file_changes, '/TestExperiment/Directory_1/Group_1/file_02.txt', with_metadata=True) - response = self.client.put('/rest/v2/datasets/%d/files/user_metadata' % cr_id, file_changes, format="json") + self._add_file( + file_changes, + "/TestExperiment/Directory_1/Group_1/file_02.txt", + with_metadata=True, + ) + response = self.client.put( + "/rest/v2/datasets/%d/files/user_metadata" % cr_id, + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - file_data = CR.objects.only('research_dataset').get(pk=cr_id).research_dataset['files'][0] - file_id = file_data['identifier'] + file_data = CR.objects.only("research_dataset").get(pk=cr_id).research_dataset["files"][0] + file_id = file_data["identifier"] # delete data of all files - file_changes = {'files': []} - file_changes['files'].append({'delete': True}) - file_changes['files'][0]['identifier'] = file_id - - response = self.client.put('/rest/v2/datasets/%d/files/user_metadata' % cr_id, file_changes, format="json") + file_changes = {"files": []} + file_changes["files"].append({"delete": True}) + file_changes["files"][0]["identifier"] = file_id + + response = self.client.put( + "/rest/v2/datasets/%d/files/user_metadata" % cr_id, + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - cr = CR.objects.only('research_dataset').get(pk=cr_id) + cr = CR.objects.only("research_dataset").get(pk=cr_id) - self.assertFalse(cr.research_dataset.get('files', False), 'file metadata must not be there') + self.assertFalse(cr.research_dataset.get("files", False), "file metadata must not be there") # add metadata for one directory file_changes = {} - self._add_directory(file_changes, '/TestExperiment', with_metadata=True) - response = self.client.put('/rest/v2/datasets/%d/files/user_metadata' % cr_id, file_changes, format="json") + self._add_directory(file_changes, "/TestExperiment", with_metadata=True) + response = self.client.put( + "/rest/v2/datasets/%d/files/user_metadata" % cr_id, + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - dir_data = CR.objects.only('research_dataset').get(pk=cr_id).research_dataset['directories'][0] + dir_data = ( + CR.objects.only("research_dataset").get(pk=cr_id).research_dataset["directories"][0] + ) - dir_id = dir_data['identifier'] + dir_id = dir_data["identifier"] # delete data of all directories - file_changes = {'directories': []} - file_changes['directories'].append({'delete': True}) - file_changes['directories'][0]['identifier'] = dir_id - - response = self.client.put('/rest/v2/datasets/%d/files/user_metadata' % cr_id, file_changes, format="json") + file_changes = {"directories": []} + file_changes["directories"].append({"delete": True}) + file_changes["directories"][0]["identifier"] = dir_id + + response = self.client.put( + "/rest/v2/datasets/%d/files/user_metadata" % cr_id, + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - cr = CR.objects.only('research_dataset').get(pk=cr_id) + cr = CR.objects.only("research_dataset").get(pk=cr_id) - self.assertFalse(cr.research_dataset.get('directories', False), 'directory metadata must not be there') - self.assertFalse(cr.research_dataset.get('files', False), 'file metadata must not be there') + self.assertFalse( + cr.research_dataset.get("directories", False), + "directory metadata must not be there", + ) + self.assertFalse(cr.research_dataset.get("files", False), "file metadata must not be there") class CatalogRecordFileHandlingCumulativeDatasets(CatalogRecordApiWriteAssignFilesCommonV2): @@ -1020,23 +1188,25 @@ class CatalogRecordFileHandlingCumulativeDatasets(CatalogRecordApiWriteAssignFil def setUp(self): super().setUp() - self.cr_test_data.pop('files', None) - self.cr_test_data.pop('directories', None) - self.cr_test_data['cumulative_state'] = CR.CUMULATIVE_STATE_YES - self._add_file(self.cr_test_data, '/TestExperiment/Directory_1/file_05.txt') + self.cr_test_data.pop("files", None) + self.cr_test_data.pop("directories", None) + self.cr_test_data["cumulative_state"] = CR.CUMULATIVE_STATE_YES + self._add_file(self.cr_test_data, "/TestExperiment/Directory_1/file_05.txt") - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.cr_id = response.data['id'] + self.cr_id = response.data["id"] def test_add_files_to_cumulative_dataset(self): """ Adding files to an existing cumulative dataset should be ok. """ file_data = {} - self._add_file(file_data, '/TestExperiment/Directory_1/file_06.txt') + self._add_file(file_data, "/TestExperiment/Directory_1/file_06.txt") - response = self.client.post('/rest/v2/datasets/%d/files' % self.cr_id, file_data, format="json") + response = self.client.post( + "/rest/v2/datasets/%d/files" % self.cr_id, file_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr = CR.objects.get(pk=self.cr_id) self.assertEqual(cr.files.count(), 2) @@ -1047,26 +1217,34 @@ def test_exclude_files_from_cumulative_dataset(self): Excluding files from an existing cumulative dataset should be prevented. """ file_data = {} - self._exclude_file(file_data, '/TestExperiment/Directory_1/file_05.txt') + self._exclude_file(file_data, "/TestExperiment/Directory_1/file_05.txt") - response = self.client.post('/rest/v2/datasets/%d/files' % self.cr_id, file_data, format="json") + response = self.client.post( + "/rest/v2/datasets/%d/files" % self.cr_id, file_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('Excluding files from a cumulative' in response.data['detail'][0], True, response.data) + self.assertEqual( + "Excluding files from a cumulative" in response.data["detail"][0], + True, + response.data, + ) self.assertEqual(CR.objects.get(pk=self.cr_id).files.count(), 1) def test_change_cumulative_states_on_draft(self): """ Ensure changing cumulative states on a new cr draft works as expected. """ - response = self.client.post('/rest/v2/datasets?draft=true', self.cr_test_data, format="json") + response = self.client.post( + "/rest/v2/datasets?draft=true", self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] + cr_id = response.data["id"] # set to NO -> should remove all trace of ever being cumulative response = self.client.post( - f'/rpc/v2/datasets/change_cumulative_state?identifier={cr_id}&cumulative_state={CR.CUMULATIVE_STATE_NO}', - format="json" + f"/rpc/v2/datasets/change_cumulative_state?identifier={cr_id}&cumulative_state={CR.CUMULATIVE_STATE_NO}", + format="json", ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) @@ -1078,8 +1256,8 @@ def test_change_cumulative_states_on_draft(self): # set back to YES response = self.client.post( - f'/rpc/v2/datasets/change_cumulative_state?identifier={cr_id}&cumulative_state={CR.CUMULATIVE_STATE_YES}', - format="json" + f"/rpc/v2/datasets/change_cumulative_state?identifier={cr_id}&cumulative_state={CR.CUMULATIVE_STATE_YES}", + format="json", ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) @@ -1092,13 +1270,15 @@ def test_change_cumulative_states_on_draft(self): # set back to CLOSED -> does not make sense on a draft # note: too long url... yes its ugly response = self.client.post( - '/rpc/v2/datasets/change_cumulative_state' - f'?identifier={cr_id}&cumulative_state={CR.CUMULATIVE_STATE_CLOSED}', - format="json" + "/rpc/v2/datasets/change_cumulative_state" + f"?identifier={cr_id}&cumulative_state={CR.CUMULATIVE_STATE_CLOSED}", + format="json", ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) self.assertEqual( - 'For a new dataset, cumulative_state must be' in response.data['detail'][0], True, response.data + "For a new dataset, cumulative_state must be" in response.data["detail"][0], + True, + response.data, ) def test_change_cumulative_states_on_published_draft(self): @@ -1108,40 +1288,45 @@ def test_change_cumulative_states_on_published_draft(self): """ # create draft of the cumulative dataset - response = self.client.post(f'/rpc/v2/datasets/create_draft?identifier={self.cr_id}', format="json") + response = self.client.post( + f"/rpc/v2/datasets/create_draft?identifier={self.cr_id}", format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - draft_id = response.data['id'] + draft_id = response.data["id"] # set to NO -> should be prevented response = self.client.post( - '/rpc/v2/datasets/change_cumulative_state' - f'?identifier={draft_id}&cumulative_state={CR.CUMULATIVE_STATE_NO}', - format="json" + "/rpc/v2/datasets/change_cumulative_state" + f"?identifier={draft_id}&cumulative_state={CR.CUMULATIVE_STATE_NO}", + format="json", ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) self.assertTrue( - 'Cumulative dataset cannot be set to non-cumulative' in response.data['detail'][0], response.data + "Cumulative dataset cannot be set to non-cumulative" in response.data["detail"][0], + response.data, ) # set to YES -> should do nothing, since is already cumulative response = self.client.post( - '/rpc/v2/datasets/change_cumulative_state' - f'?identifier={draft_id}&cumulative_state={CR.CUMULATIVE_STATE_YES}', - format="json" + "/rpc/v2/datasets/change_cumulative_state" + f"?identifier={draft_id}&cumulative_state={CR.CUMULATIVE_STATE_YES}", + format="json", ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) # set to CLOSED -> should work response = self.client.post( - '/rpc/v2/datasets/change_cumulative_state' - f'?identifier={draft_id}&cumulative_state={CR.CUMULATIVE_STATE_CLOSED}', - format="json" + "/rpc/v2/datasets/change_cumulative_state" + f"?identifier={draft_id}&cumulative_state={CR.CUMULATIVE_STATE_CLOSED}", + format="json", ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) # merge draft changes - response = self.client.post(f'/rpc/v2/datasets/merge_draft?identifier={draft_id}', format="json") + response = self.client.post( + f"/rpc/v2/datasets/merge_draft?identifier={draft_id}", format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # check updates were applied @@ -1157,25 +1342,31 @@ def test_add_files_to_cumulative_published_draft_dataset(self): num_files_added = 1 # create draft of the cumulative dataset - response = self.client.post('/rpc/v2/datasets/create_draft?identifier=%d' % self.cr_id, format="json") + response = self.client.post( + "/rpc/v2/datasets/create_draft?identifier=%d" % self.cr_id, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - draft_id = response.data['id'] + draft_id = response.data["id"] # add files to draft file_data = {} - self._add_file(file_data, '/TestExperiment/Directory_1/Group_1/file_01.txt') - self._add_file(file_data, '/TestExperiment/Directory_1/Group_1/file_02.txt') + self._add_file(file_data, "/TestExperiment/Directory_1/Group_1/file_01.txt") + self._add_file(file_data, "/TestExperiment/Directory_1/Group_1/file_02.txt") - num_files_added += len(file_data['files']) + num_files_added += len(file_data["files"]) - response = self.client.post('/rest/v2/datasets/%d/files' % draft_id, file_data, format="json") + response = self.client.post( + "/rest/v2/datasets/%d/files" % draft_id, file_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # ensure excluding is prevented file_data = {} - self._exclude_file(file_data, '/TestExperiment/Directory_1/Group_1/file_02.txt') - response = self.client.post('/rest/v2/datasets/%d/files' % draft_id, file_data, format="json") + self._exclude_file(file_data, "/TestExperiment/Directory_1/Group_1/file_02.txt") + response = self.client.post( + "/rest/v2/datasets/%d/files" % draft_id, file_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # ensure files were added to draft @@ -1187,7 +1378,9 @@ def test_add_files_to_cumulative_published_draft_dataset(self): self.assertEqual(cr.files.count(), 1) # merge draft changes - response = self.client.post('/rpc/v2/datasets/merge_draft?identifier=%d' % draft_id, format="json") + response = self.client.post( + "/rpc/v2/datasets/merge_draft?identifier=%d" % draft_id, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # published dataset should now have files added @@ -1195,21 +1388,25 @@ def test_add_files_to_cumulative_published_draft_dataset(self): self.assertEqual(cr.files.count(), num_files_added) # create another draft - response = self.client.post('/rpc/v2/datasets/create_draft?identifier=%d' % self.cr_id, format="json") + response = self.client.post( + "/rpc/v2/datasets/create_draft?identifier=%d" % self.cr_id, format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - draft_id = response.data['id'] + draft_id = response.data["id"] # close cumulative period on draft response = self.client.post( - '/rpc/v2/datasets/change_cumulative_state' - f'?identifier={draft_id}&cumulative_state={CR.CUMULATIVE_STATE_CLOSED}', - format="json" + "/rpc/v2/datasets/change_cumulative_state" + f"?identifier={draft_id}&cumulative_state={CR.CUMULATIVE_STATE_CLOSED}", + format="json", ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) # merge draft changes again - response = self.client.post('/rpc/v2/datasets/merge_draft?identifier=%d' % draft_id, format="json") + response = self.client.post( + "/rpc/v2/datasets/merge_draft?identifier=%d" % draft_id, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # published dataset should now have cumulativity closed @@ -1220,6 +1417,6 @@ def test_change_preservation_state(self): """ PAS process should not be started while cumulative period is open. """ - cr = { 'preservation_state': 10 } - response = self.client.patch('/rest/v2/datasets/%s' % self.cr_id, cr, format="json") + cr = {"preservation_state": 10} + response = self.client.patch("/rest/v2/datasets/%s" % self.cr_id, cr, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/pas.py b/src/metax_api/tests/api/rest/v2/views/datasets/pas.py index 17f23ab3..365782d9 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/pas.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/pas.py @@ -5,126 +5,15 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from datetime import timedelta - from django.conf import settings as django_settings from rest_framework import status -from metax_api.models import CatalogRecordV2, Contract, DataCatalog +from metax_api.models import CatalogRecordV2, DataCatalog from metax_api.utils import get_tz_aware_now_without_micros -from .read import CatalogRecordApiReadCommon from .write import CatalogRecordApiWriteCommon -class CatalogRecordApiReadPreservationStateTests(CatalogRecordApiReadCommon): - - """ - preservation_state filtering - """ - - def test_read_catalog_record_search_by_preservation_state(self): - ''' - Various simple filtering requests - ''' - response = self.client.get('/rest/v2/datasets?preservation_state=0') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']) > 2, True, - 'There should have been multiple results for preservation_state=0 request') - - response = self.client.get('/rest/v2/datasets?preservation_state=10') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 2) - - response = self.client.get('/rest/v2/datasets?preservation_state=40') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) - - def test_read_catalog_record_search_by_preservation_state_666(self): - response = self.client.get('/rest/v2/datasets?preservation_state=666') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 0, 'should return empty list') - - def test_read_catalog_record_search_by_preservation_state_many(self): - response = self.client.get('/rest/v2/datasets?preservation_state=10,40') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 3) - self.assertEqual(response.data['results'][0]['preservation_state'], 10) - self.assertEqual(response.data['results'][1]['preservation_state'], 10) - self.assertEqual(response.data['results'][2]['preservation_state'], 40) - - def test_read_catalog_record_search_by_preservation_state_invalid_value(self): - response = self.client.get('/rest/v2/datasets?preservation_state=1,a') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('is not an integer' in response.data['preservation_state'][0], True, - 'Error should say letter a is not an integer') - - response = self.client.get('/rest/v2/datasets?preservation_state=1,a') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('is not an integer' in response.data['preservation_state'][0], True, - 'Error should say letter a is not an integer') - - -class CatalogRecordApiReadPASFilter(CatalogRecordApiReadCommon): - - def test_pas_filter(self): - """ - Test query param pas_filter which should search from various fields using the same search term. - """ - - # set test conditions - cr = CatalogRecordV2.objects.get(pk=1) - cr.preservation_state = 10 - cr.contract_id = 1 - cr.research_dataset['title']['en'] = 'Catch me if you can' - cr.research_dataset['title']['fi'] = 'Ota kiinni jos saat' - cr.research_dataset['curator'] = [] - cr.research_dataset['curator'].append({ 'name': 'Seppo Hovi' }) - cr.research_dataset['curator'].append({ 'name': 'Esa Nieminen' }) - cr.research_dataset['curator'].append({ 'name': 'Aku Ankka' }) - cr.research_dataset['curator'].append({ 'name': 'Jaska Jokunen' }) - cr.force_save() - - contract = Contract.objects.get(pk=1) - contract.contract_json['title'] = 'An Important Agreement' - contract.save() - - metax_user = django_settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) - - # beging testing - - response = self.client.get('/rest/v2/datasets?preservation_state=10&pas_filter=if you') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) - - response = self.client.get('/rest/v2/datasets?preservation_state=10&pas_filter=kiinni jos') - self.assertEqual(len(response.data['results']), 1) - - response = self.client.get('/rest/v2/datasets?preservation_state=10&pas_filter=niemine') - self.assertEqual(len(response.data['results']), 1) - - # more than 3 curators, requires typing exact case-sensitive name... see comments in related code - response = self.client.get('/rest/v2/datasets?preservation_state=10&pas_filter=jokunen') - self.assertEqual(len(response.data['results']), 0) - response = self.client.get('/rest/v2/datasets?preservation_state=10&pas_filter=Jaska Jokunen') - self.assertEqual(len(response.data['results']), 1) - - # contract_id 1 has several other associated test datasets - response = self.client.get('/rest/v2/datasets?preservation_state=10&pas_filter=agreement') - self.assertEqual(len(response.data['results']), 3) - - response = self.client.get('/rest/v2/datasets?preservation_state=10&pas_filter=does not exist') - self.assertEqual(len(response.data['results']), 0) - - def test_pas_filter_is_restricted(self): - """ - Query param is permitted to users metax and tpas. - """ - response = self.client.get('/rest/v2/datasets?preservation_state=10&pas_filter=hmmm') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - class CatalogRecordApiWritePreservationStateTests(CatalogRecordApiWriteCommon): """ @@ -136,12 +25,12 @@ def _create_pas_dataset_from_id(self, id): Helper method to create a pas dataset by updating the given dataset's preservation_state to 80. """ - cr_data = self.client.get('/rest/v2/datasets/%d' % id, format="json").data - self.assertEqual(cr_data['preservation_state'], 0) + cr_data = self.client.get("/rest/v2/datasets/%d" % id, format="json").data + self.assertEqual(cr_data["preservation_state"], 0) # update state to "accepted to pas" -> should create pas version - cr_data['preservation_state'] = 80 - response = self.client.put('/rest/v2/datasets/%d' % id, cr_data, format="json") + cr_data["preservation_state"] = 80 + response = self.client.put("/rest/v2/datasets/%d" % id, cr_data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) return response.data @@ -149,25 +38,15 @@ def setUp(self): super().setUp() dc = DataCatalog.objects.get(pk=1) catalog_json = dc.catalog_json - catalog_json['identifier'] = django_settings.PAS_DATA_CATALOG_IDENTIFIER - catalog_json['dataset_versioning'] = False + catalog_json["identifier"] = django_settings.PAS_DATA_CATALOG_IDENTIFIER + catalog_json["dataset_versioning"] = False dc = DataCatalog.objects.create( catalog_json=catalog_json, date_created=get_tz_aware_now_without_micros(), - catalog_record_services_create='testuser,api_auth_user,metax', - catalog_record_services_edit='testuser,api_auth_user,metax' + catalog_record_services_create="testuser,api_auth_user,metax", + catalog_record_services_edit="testuser,api_auth_user,metax", ) - def test_update_catalog_record_pas_state_allowed_value(self): - cr = self.client.get('/rest/v2/datasets/1').data - cr['preservation_state'] = 30 - response = self.client.put('/rest/v2/datasets/1', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - cr = CatalogRecordV2.objects.get(pk=1) - self.assertEqual(cr.preservation_state_modified >= get_tz_aware_now_without_micros() - timedelta(seconds=5), - True, 'Timestamp should have been updated during object update') - def test_update_pas_state_to_needs_revalidation(self): """ When dataset metadata is updated, and preservation_state in (40, 50, 70), metax should @@ -181,14 +60,14 @@ def test_update_pas_state_to_needs_revalidation(self): cr.save() # retrieve record and ensure testing state was set correctly... - cr_data = self.client.get('/rest/v2/datasets/1', format="json").data - self.assertEqual(cr_data['preservation_state'], preservation_state_value) + cr_data = self.client.get("/rest/v2/datasets/1", format="json").data + self.assertEqual(cr_data["preservation_state"], preservation_state_value) # strike and verify - cr_data['research_dataset']['title']['en'] = 'Metadata has been updated on loop %d' % i - response = self.client.put('/rest/v2/datasets/1', cr_data, format="json") + cr_data["research_dataset"]["title"]["en"] = "Metadata has been updated on loop %d" % i + response = self.client.put("/rest/v2/datasets/1", cr_data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['preservation_state'], 60) + self.assertEqual(response.data["preservation_state"], 60) def test_prevent_file_changes_when_record_in_pas_process(self): """ @@ -198,137 +77,72 @@ def test_prevent_file_changes_when_record_in_pas_process(self): cr.preservation_state = 10 cr.save() - file_changes = { - 'files': [{ 'identifier': 'pid:urn:3' }] - } + file_changes = {"files": [{"identifier": "pid:urn:3"}]} - response = self.client.post('/rest/v2/datasets/1/files', file_changes, format="json") + response = self.client.post("/rest/v2/datasets/1/files", file_changes, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('Changing files of a published' in response.data['detail'][0], True, response.data) - - def test_non_pas_dataset_unallowed_preservation_state_values(self): - # update non-pas dataset - cr = self.client.get('/rest/v2/datasets/1').data - - values = [ - 11, # not one of known values - 90, # value not allowed for non-pas datasets - ] - - for invalid_value in values: - cr['preservation_state'] = invalid_value - response = self.client.put('/rest/v2/datasets/1', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - - def test_pas_dataset_unallowed_preservation_state_values(self): - # create pas dataset and update with invalid values - cr = self.client.get('/rest/v2/datasets/1').data - cr['preservation_state'] = 80 - response = self.client.put('/rest/v2/datasets/1', cr, format="json") - cr = self.client.get('/rest/v2/datasets/%d' % response.data['preservation_dataset_version']['id']).data - - values = [ - 70, # value not allowed for non-pas datasets - 111, # not one of known values - 150 # not one of known values - ] - - for invalid_value in values: - cr['preservation_state'] = invalid_value - response = self.client.put('/rest/v2/datasets/1', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + self.assertEqual( + "Changing files of a published" in response.data["detail"][0], + True, + response.data, + ) def test_pas_version_is_created_on_preservation_state_80(self): """ When preservation_state is updated to 'accepted to pas', a copy should be created into designated PAS catalog. """ - cr_data = self.client.get('/rest/v2/datasets/1', format="json").data - self.assertEqual(cr_data['preservation_state'], 0) + cr_data = self.client.get("/rest/v2/datasets/1", format="json").data + self.assertEqual(cr_data["preservation_state"], 0) origin_dataset = self._create_pas_dataset_from_id(1) - self.assertEqual(origin_dataset['preservation_state'], 0) - self.assertEqual('new_version_created' in origin_dataset, True) - self.assertEqual(origin_dataset['new_version_created']['version_type'], 'pas') - self.assertEqual('preservation_dataset_version' in origin_dataset, True) - self.assertEqual('other_identifier' in origin_dataset['research_dataset'], True) - self.assertEqual(origin_dataset['research_dataset']['other_identifier'][0]['notation'].startswith('doi'), True) + self.assertEqual(origin_dataset["preservation_state"], 0) + self.assertEqual("new_version_created" in origin_dataset, True) + self.assertEqual(origin_dataset["new_version_created"]["version_type"], "pas") + self.assertEqual("preservation_dataset_version" in origin_dataset, True) + self.assertEqual("other_identifier" in origin_dataset["research_dataset"], True) + self.assertEqual( + origin_dataset["research_dataset"]["other_identifier"][0]["notation"].startswith("doi"), + True, + ) # get pas version and verify links and other signature values are there pas_dataset = self.client.get( - '/rest/v2/datasets/%d' % origin_dataset['preservation_dataset_version']['id'], format="json" + "/rest/v2/datasets/%d" % origin_dataset["preservation_dataset_version"]["id"], + format="json", ).data - self.assertEqual(pas_dataset['data_catalog']['identifier'], django_settings.PAS_DATA_CATALOG_IDENTIFIER) - self.assertEqual(pas_dataset['preservation_state'], 80) - self.assertEqual(pas_dataset['preservation_dataset_origin_version']['id'], origin_dataset['id']) self.assertEqual( - pas_dataset['preservation_dataset_origin_version']['preferred_identifier'], - origin_dataset['research_dataset']['preferred_identifier'] + pas_dataset["data_catalog"]["identifier"], + django_settings.PAS_DATA_CATALOG_IDENTIFIER, + ) + self.assertEqual(pas_dataset["preservation_state"], 80) + self.assertEqual( + pas_dataset["preservation_dataset_origin_version"]["id"], + origin_dataset["id"], + ) + self.assertEqual( + pas_dataset["preservation_dataset_origin_version"]["preferred_identifier"], + origin_dataset["research_dataset"]["preferred_identifier"], + ) + self.assertEqual("deprecated" in pas_dataset["preservation_dataset_origin_version"], True) + self.assertEqual("other_identifier" in pas_dataset["research_dataset"], True) + self.assertEqual( + pas_dataset["research_dataset"]["other_identifier"][0]["notation"].startswith("urn"), + True, ) - self.assertEqual('deprecated' in pas_dataset['preservation_dataset_origin_version'], True) - self.assertEqual('other_identifier' in pas_dataset['research_dataset'], True) - self.assertEqual(pas_dataset['research_dataset']['other_identifier'][0]['notation'].startswith('urn'), True) # when pas copy is created, origin_dataset preservation_state should have been set back to 0 - cr_data = self.client.get('/rest/v2/datasets/1', format="json").data - self.assertEqual(cr_data['preservation_state'], 0) + cr_data = self.client.get("/rest/v2/datasets/1", format="json").data + self.assertEqual(cr_data["preservation_state"], 0) # ensure files match between original and pas cr cr = CatalogRecordV2.objects.get(pk=1) - cr_files = cr.files.filter().order_by('id').values_list('id', flat=True) - cr_pas_files = cr.preservation_dataset_version.files.filter().order_by('id').values_list('id', flat=True) - - # note: trying to assert querysets will result in failure. must evaluate the querysets first by iterating them - self.assertEqual([f for f in cr_files], [f for f in cr_pas_files]) - - def test_origin_dataset_cant_have_multiple_pas_versions(self): - """ - If state is update to 'accepted to pas', and relation preservation_dataset_version - is detected, an error should be raised. - """ - self._create_pas_dataset_from_id(1) - - cr_data = { 'preservation_state': 80 } - response = self.client.patch('/rest/v2/datasets/1', cr_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('already has a PAS version' in response.data['detail'][0], True, response.data) - - def test_dataset_can_be_created_directly_into_pas_catalog(self): - """ - Datasets that are created directly into PAS catalog should not have any enforced - rules about changing preservation_state value. - """ - self.cr_test_data['data_catalog'] = django_settings.PAS_DATA_CATALOG_IDENTIFIER - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual( - response.data['research_dataset']['preferred_identifier'].startswith('doi'), - True, - response.data['research_dataset']['preferred_identifier'] + cr_files = cr.files.filter().order_by("id").values_list("id", flat=True) + cr_pas_files = ( + cr.preservation_dataset_version.files.filter() + .order_by("id") + .values_list("id", flat=True) ) - # when created directly into pas catalog, preservation_state can be updated - # to whatever, whenever - ps_values = [ v[0] for v in CatalogRecordV2.PRESERVATION_STATE_CHOICES ] - for ps in ps_values: - cr_data = { 'preservation_state': ps } - response = self.client.patch('/rest/v2/datasets/%d' % response.data['id'], cr_data, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - cr_data = { 'preservation_state': 0 } - response = self.client.patch('/rest/v2/datasets/%d' % response.data['id'], cr_data, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_unfreezing_files_does_not_deprecate_pas_dataset(self): - """ - Even if the origin dataset is deprecated as a result of unfreezing its files, - the PAS dataset should be safe from being deprecated, as the files have already - been stored in PAS. - """ - cr = self._create_pas_dataset_from_id(1) - response = self.client.delete('/rest/v2/files/1', format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - response = self.client.get('/rest/v2/datasets/%d' % cr['preservation_dataset_version']['id'], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['deprecated'], False) + # note: trying to assert querysets will result in failure. must evaluate the querysets first by iterating them + self.assertEqual([f for f in cr_files], [f for f in cr_pas_files]) \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/read.py b/src/metax_api/tests/api/rest/v2/views/datasets/read.py index 6c75abe7..7e3bd197 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/read.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/read.py @@ -6,14 +6,9 @@ # :license: MIT from copy import deepcopy -import urllib.parse -from datetime import timedelta -import responses from django.conf import settings from django.core.management import call_command -from django.utils import timezone -from pytz import timezone as tz from rest_framework import status from rest_framework.test import APITestCase @@ -27,239 +22,32 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(CatalogRecordApiReadCommon, cls).setUpClass() def setUp(self): - self.cr_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=0) - self.pk = self.cr_from_test_data['id'] - self.metadata_version_identifier = self.cr_from_test_data['research_dataset']['metadata_version_identifier'] - self.preferred_identifier = self.cr_from_test_data['research_dataset']['preferred_identifier'] - self.identifier = self.cr_from_test_data['identifier'] + self.cr_from_test_data = self._get_object_from_test_data("catalogrecord", requested_index=0) + self.pk = self.cr_from_test_data["id"] + self.metadata_version_identifier = self.cr_from_test_data["research_dataset"][ + "metadata_version_identifier" + ] + self.preferred_identifier = self.cr_from_test_data["research_dataset"][ + "preferred_identifier" + ] + self.identifier = self.cr_from_test_data["identifier"] self._use_http_authorization() def create_legacy_dataset(self): cr = deepcopy(self.cr_from_test_data) - cr['data_catalog'] = settings.LEGACY_CATALOGS[0] - cr.pop('identifier') - cr['research_dataset']['preferred_identifier'] = 'ldhkrfdwam' - cr['research_dataset'].pop('files') - cr['research_dataset'].pop('total_files_byte_size') - response = self.client.post('/rest/v2/datasets', cr, format="json") + cr["data_catalog"] = settings.LEGACY_CATALOGS[0] + cr.pop("identifier") + cr["research_dataset"]["preferred_identifier"] = "ldhkrfdwam" + cr["research_dataset"].pop("files") + cr["research_dataset"].pop("total_files_byte_size") + response = self.client.post("/rest/v2/datasets", cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - return response.data['id'] - - -class CatalogRecordApiReadBasicTests(CatalogRecordApiReadCommon): - - """ - Basic read operations - """ - - def test_read_catalog_record_list(self): - response = self.client.get('/rest/v2/datasets') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_read_catalog_record_list_by_ids(self): - # Successful tests - - cr_id_lists = [ - [1, 4, 6], - [1, 4, 6, 777], - ['cr955e904-e3dd-4d7e-99f1-3fed446f96d7', - 'cr955e904-e3dd-4d7e-99f1-3fed446f96d6', - 'cr955e904-e3dd-4d7e-99f1-3fed446f96d5'], - ['cr955e904-e3dd-4d7e-99f1-3fed446f96d7', - 'cr955e904-e3dd-4d7e-99f1-3fed446f96d6', - 'cr955e904-e3dd-4d7e-99f1-3fed446f96d5', - 'something'] - ] - - for id_list in cr_id_lists: - response = self.client.post('/rest/v2/datasets/list', id_list, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 3) - - # check that fields parameter works - response = self.client.post('/rest/v2/datasets/list?fields=id', id_list, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 3) - self.assertEqual(len(response.data['results'][0].keys()), 1) - self.assertEqual(list(response.data['results'][0].keys()), ['id']) - - # Failing/empty tests - - cr_bad_lists = [ - ['something'], - [999, 777] - ] - - for bad_list in cr_bad_lists: - response = self.client.post('/rest/v2/datasets/list', bad_list, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['results'], []) - - response = self.client.post('/rest/v2/datasets/list', [], format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('Received empty list of identifiers' in response.data['detail']) - - def test_read_catalog_record_details_by_pk(self): - response = self.client.get('/rest/v2/datasets/%s' % self.pk) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['identifier'], self.identifier) - self.assertEqual('identifier' in response.data['data_catalog'], True) - - def test_read_catalog_record_details_by_identifier(self): - response = self.client.get('/rest/v2/datasets/%s' % self.identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['identifier'], - self.identifier) - - def test_get_by_preferred_identifier(self): - cr = CatalogRecordV2.objects.get(pk=1) - cr.research_dataset['preferred_identifier'] = '%s-/uhoh/special.chars?all&around' % cr.preferred_identifier - cr.force_save() - response = self.client.get('/rest/v2/datasets?preferred_identifier=%s' % - urllib.parse.quote(cr.preferred_identifier)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], cr.preferred_identifier) - - def test_get_removed_by_preferred_identifier(self): - self._use_http_authorization() - response = self.client.delete('/rest/v2/datasets/%s' % self.identifier) - - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - response = self.client.get('/rest/v2/datasets?preferred_identifier=%s&removed=true' % - urllib.parse.quote(self.preferred_identifier)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_get_by_preferred_identifier_search_prefers_oldest_data_catalog(self): - ''' - Search by preferred_identifier should prefer hits from oldest created catalogs - which are assumed to be att/fairdata catalogs. - ''' - - # get a cr that has alternate records - cr = self._get_object_from_test_data('catalogrecord', requested_index=9) - pid = cr['research_dataset']['preferred_identifier'] - - # verify there are more than one record with same pid! - count = CatalogRecordV2.objects.filter(research_dataset__preferred_identifier=pid).count() - self.assertEqual(count > 1, True, 'makes no sense to test with a pid that exists only once') - - # the retrieved record should be the one that is in catalog 1 - response = self.client.get('/rest/v2/datasets?preferred_identifier=%s' % - urllib.parse.quote(pid)) - self.assertEqual('alternate_record_set' in response.data, True) - self.assertEqual(response.data['data_catalog']['id'], cr['data_catalog']) - - def test_read_catalog_record_details_not_found(self): - response = self.client.get('/rest/v2/datasets/shouldnotexist') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_read_catalog_record_metadata_version_identifiers(self): - response = self.client.get('/rest/v2/datasets/metadata_version_identifiers') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(isinstance(response.data, list)) - self.assertTrue(len(response.data) > 0) - - def test_get_unique_preferred_identifiers(self): - """ - Get all unique preferred_identifiers, no matter if they are the latest dataset version or not. - """ - response = self.client.get('/rest/v2/datasets/unique_preferred_identifiers') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(isinstance(response.data, list)) - self.assertTrue(len(response.data) > 0) - - # save the current len, do some more operations, and compare the difference - ids_len = len(response.data) - - self._create_new_ds() - self._create_new_ds() - response = self.client.get('/rest/v2/datasets/unique_preferred_identifiers') - self.assertEqual(len(response.data) - ids_len, 2, 'should be two new PIDs') - - def test_get_latest_unique_preferred_identifiers(self): - """ - Get all unique preferred_identifiers, but only from the latest dataset versions. - """ - response = self.client.get('/rest/v2/datasets/unique_preferred_identifiers?latest') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(isinstance(response.data, list)) - self.assertTrue(len(response.data) > 0) - - # save the current len, do some more operations, and compare the difference - ids_len = len(response.data) - - # files change - cr = CatalogRecordV2.objects.get(pk=1) - new_file_id = cr.files.all().order_by('-id').first().id + 1 - file_from_testdata = self._get_object_from_test_data('file', requested_index=new_file_id) - # warning, this is actual file metadata, would not pass schema validation if sent through api - cr.research_dataset['files'] = [file_from_testdata] - cr.save() - response = self.client.get('/rest/v2/datasets/unique_preferred_identifiers?latest') - self.assertEqual(ids_len, len(response.data), 'count should stay the same') - - # create new - self._create_new_ds() - self._create_new_ds() - response = self.client.get('/rest/v2/datasets/unique_preferred_identifiers?latest') - self.assertEqual(len(response.data) - ids_len, 2, 'should be two new PIDs') - - def test_expand_relations(self): - cr = CatalogRecordV2.objects.get(pk=1) - cr.contract_id = 1 - cr.force_save() - - response = self.client.get('/rest/v2/datasets/1?expand_relation=data_catalog,contract') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('catalog_json' in response.data['data_catalog'], True, response.data['data_catalog']) - self.assertEqual('contract_json' in response.data['contract'], True, response.data['contract']) - - def test_strip_sensitive_fields(self): - """ - Strip fields not intended for general public - """ - def _check_fields(obj): - for sensitive_field in ['email', 'telephone', 'phone']: - self.assertEqual(sensitive_field not in obj['research_dataset']['curator'][0], True, - 'field %s should have been stripped' % sensitive_field) - - for cr in CatalogRecordV2.objects.filter(pk__in=(1, 2, 3)): - cr.research_dataset['curator'][0].update({ - 'email': 'email@mail.com', - 'phone': '123124', - 'telephone': '123124', - }) - cr.force_save() - - self.client._credentials = {} - - response = self.client.get('/rest/v2/datasets/1') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - _check_fields(response.data) - - response = self.client.get('/rest/v2/datasets') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - for obj in response.data['results']: - _check_fields(obj) - - response = self.client.get('/rest/datasets?pagination=false') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - for obj in response.data: - _check_fields(obj) - - def _create_new_ds(self): - new_cr = self.client.get('/rest/v2/datasets/2').data - new_cr.pop('id') - new_cr['research_dataset'].pop('preferred_identifier') - new_cr.pop('identifier') - self._use_http_authorization() - response = self.client.post('/rest/v2/datasets', new_cr, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + return response.data["id"] class CatalogRecordApiReadBasicAuthorizationTests(CatalogRecordApiReadCommon): @@ -268,528 +56,44 @@ class CatalogRecordApiReadBasicAuthorizationTests(CatalogRecordApiReadCommon): Basic read operations from authorization perspective """ - api_version = 'v2' - - # THE OK TESTS - - def test_returns_all_file_dir_info_for_open_catalog_record_if_no_authorization(self): - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify all file and dir details info is returned for open cr /rest/v2/datasets/ without - # authorization - self._assert_ok(open_cr_json, 'no') - - def test_returns_all_file_dir_info_for_login_catalog_record_if_no_authorization(self): - login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(use_login_access_type=True) - - # Verify all file and dir details info is returned for login cr /rest/v2/datasets/ without authorization - self._assert_ok(login_cr_json, 'no') - - @responses.activate - def test_returns_all_file_dir_info_for_open_catalog_record_if_owner_authorization(self): - self.create_end_user_data_catalogs() - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify all file and dir details info is returned for open owner-owned cr /rest/v2/datasets/ with - # owner authorization - self._assert_ok(open_cr_json, 'owner') - - @responses.activate - def test_returns_all_file_dir_info_for_login_catalog_record_if_owner_authorization(self): - self.create_end_user_data_catalogs() - login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(set_owner=True, - use_login_access_type=True) - - # Verify all file and dir details info is returned for login owner-owned cr /rest/v2/datasets/ with - # owner authorization - self._assert_ok(login_cr_json, 'owner') - - def test_returns_all_file_dir_info_for_restricted_catalog_record_if_service_authorization(self): - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify all file and dir details info is returned for restricted cr /rest/v2/datasets/ with - # service authorization - self._assert_ok(restricted_cr_json, 'service') - - @responses.activate - def test_returns_all_file_dir_info_for_restricted_catalog_record_if_owner_authorization(self): - self.create_end_user_data_catalogs() - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify all file and dir details info is returned for restricted owner-owned cr /rest/v2/datasets/ with - # owner authorization - self._assert_ok(restricted_cr_json, 'owner') - - def test_returns_all_file_dir_info_for_embargoed_catalog_record_if_available_reached_and_no_authorization(self): - available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify all file and dir details info is returned for embargoed cr /rest/v2/datasets/ when - # embargo date has been reached without authorization - self._assert_ok(available_embargoed_cr_json, 'no') + api_version = "v2" # THE FORBIDDEN TESTS - def test_returns_limited_file_dir_info_for_restricted_catalog_record_if_no_authorization(self): - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify limited file and dir info for restricted cr /rest/v2/datasets/ without authorization - self._assert_limited_or_no_file_dir_info(restricted_cr_json, 'no') - - def test_no_file_dir_info_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization(self): - not_available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details( - False) + def test_no_file_dir_info_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization( + self, + ): + not_available_embargoed_cr_json = ( + self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(False) + ) # Verify no file and dir info for embargoed cr /rest/v2/datasets/ when embargo date has not # been reached without authorization - self._assert_limited_or_no_file_dir_info(not_available_embargoed_cr_json, 'no') + self._assert_limited_or_no_file_dir_info(not_available_embargoed_cr_json, "no") def _assert_limited_or_no_file_dir_info(self, cr_json, credentials_type): self._set_http_authorization(credentials_type) - file_amt = len(cr_json['research_dataset']['files']) - dir_amt = len(cr_json['research_dataset']['directories']) - pk = cr_json['id'] - - response = self.client.get('/rest/v2/datasets/{0}?include_user_metadata&file_details'.format(pk)) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - self.assertEqual(len(response.data['research_dataset']['files']), file_amt) - self.assertEqual(len(response.data['research_dataset']['directories']), dir_amt) - - for f in response.data['research_dataset']['files']: - self.assertTrue('details' in f) - # The below assert is a bit arbitrary - self.assertFalse('identifier' in f) - for d in response.data['research_dataset']['directories']: - self.assertTrue('details' in d) - # The below assert is a bit arbitrary - self.assertFalse('identifier' in d) - - def _assert_ok(self, cr_json, credentials_type): - self._set_http_authorization(credentials_type) - - file_amt = len(cr_json['research_dataset']['files']) - dir_amt = len(cr_json['research_dataset']['directories']) - pk = cr_json['id'] - - response = self.client.get('/rest/v2/datasets/{0}?include_user_metadata&file_details'.format(pk)) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['research_dataset']['files']), file_amt) - self.assertEqual(len(response.data['research_dataset']['directories']), dir_amt) - - for f in response.data['research_dataset']['files']: - self.assertTrue('details' in f) - # The below assert is a bit arbitrary - self.assertTrue('identifier' in f) - for d in response.data['research_dataset']['directories']: - self.assertTrue('details' in d) - # The below assert is a bit arbitrary - self.assertTrue('identifier' in d) - - -class CatalogRecordApiReadActorFilter(CatalogRecordApiReadCommon): - - def test_agents_and_actors(self): - # set test conditions - cr = CatalogRecordV2.objects.get(pk=11) - cr.research_dataset['curator'] = [] - cr.research_dataset['curator'].append({ - '@type': 'Person', - 'name': 'Tarmo Termiitti', - 'member_of': { - 'identifier': 'org_identifier', - 'name': { - 'en': 'Unique Organization' - } - } - }) - cr.research_dataset['curator'].append({ '@type': 'Person', 'name': 'Keijo Kottarainen' }) - cr.research_dataset['curator'].append({ '@type': 'Person', 'name': 'Janus Järvinen' }) - cr.research_dataset['curator'].append({ '@type': 'Person', 'name': 'Laina Sakkonen' }) - cr.research_dataset['curator'].append({ - '@type': 'Person', - 'name': 'Kaisa Kuraattori', - 'member_of': { - 'identifier': 'org_identifier', - 'name': { - 'en': 'Happy Organization' - } - } - }) - cr.research_dataset['creator'] = [] - cr.research_dataset['creator'].append({ '@type': 'Organization', 'name': { 'en': 'Unique Organization'} }) - cr.research_dataset['creator'].append({ '@type': 'Organization', 'name': { 'en': 'Happy Organization'} }) - cr.research_dataset['creator'].append({ '@type': 'Organization', 'name': { 'en': 'Sad Organization'} }) - cr.research_dataset['creator'].append({ '@type': 'Organization', 'name': { 'en': 'Brilliant Organization'} }) - cr.research_dataset['creator'].append({ '@type': 'Organization', 'name': {'en': 'Wonderful Organization'} }) - cr.research_dataset['publisher']['name'] = {} - cr.research_dataset['publisher']['name'] = { 'fi': 'Originaali Organisaatio' } - cr.force_save() - - response = self.client.get('/rest/v2/datasets?creator_organization=happy') - self.assertEqual(len(response.data['results']), 1, response.data) - - response = self.client.get('/rest/v2/datasets?creator_organization=Brilliant Organization') - self.assertEqual(len(response.data['results']), 1, response.data) - - response = self.client.get('/rest/v2/datasets?curator_person=termiitti') - self.assertEqual(len(response.data['results']), 1, response.data) - - response = self.client.get('/rest/v2/datasets?curator_person=Laina Sakkonen') - self.assertEqual(len(response.data['results']), 1, response.data) - - response = self.client.get('/rest/v2/datasets?curator_organization=uniqu') - self.assertEqual(len(response.data['results']), 1, response.data) - - response = self.client.get('/rest/v2/datasets?curator_organization=Happy Organization') - self.assertEqual(len(response.data['results']), 1, response.data) - - response = self.client.get('/rest/v2/datasets?publisher_organization=originaali Organisaatio') - self.assertEqual(len(response.data['results']), 1, response.data) - - query = 'curator_person=notfound&creator_organization=sad organ&condition_separator=AND' - response = self.client.get('/rest/v2/datasets?%s' % query) - self.assertEqual(len(response.data['results']), 0, response.data) - - query = 'curator_person=notfound&creator_organization=sad organ&condition_separator=OR' - response = self.client.get('/rest/v2/datasets?%s' % query) - self.assertEqual(len(response.data['results']), 1, response.data) - - # test filter with pas filter - """ - Both organization and pas filters use internally Q-filters which are supposed to be AND'ed together. - """ - metax_user = settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) - - response = self.client.get('/rest/v2/datasets?pas_filter=janus&creator_organization=sad organization') - self.assertEqual(len(response.data['results']), 1) + file_amt = len(cr_json["research_dataset"]["files"]) + dir_amt = len(cr_json["research_dataset"]["directories"]) + pk = cr_json["id"] response = self.client.get( - '/rest/v2/datasets?preservation_state=10&pas_filter=kaisa&creator_organization=notfound' + "/rest/v2/datasets/{0}?include_user_metadata&file_details".format(pk) ) - self.assertEqual(len(response.data['results']), 0) - - -class CatalogRecordApiReadQueryParamsTests(CatalogRecordApiReadCommon): - - """ - query_params filtering - """ - - def test_read_catalog_record_search_by_curator_1(self): - response = self.client.get('/rest/v2/datasets?curator=id:of:curator:rahikainen') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 10) - self.assertEqual(response.data['results'][0]['research_dataset']['curator'][0]['name'], 'Rahikainen', - 'Curator name is not matching') - self.assertEqual(response.data['results'][4]['research_dataset']['curator'][0]['name'], 'Rahikainen', - 'Curator name is not matching') - - def test_read_catalog_record_search_by_curator_2(self): - response = self.client.get('/rest/v2/datasets?curator=id:of:curator:jarski') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 4) - self.assertEqual(response.data['results'][0]['research_dataset']['curator'][0]['name'], 'Jarski', - 'Curator name is not matching') - self.assertEqual(response.data['results'][3]['research_dataset']['curator'][0]['name'], 'Jarski', - 'Curator name is not matching') - - def test_read_catalog_record_search_by_curator_not_found_1(self): - response = self.client.get('/rest/v2/datasets?curator=Not Found') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 0) - - def test_read_catalog_record_search_by_curator_not_found_case_sensitivity(self): - response = self.client.get('/rest/v2/datasets?curator=id:of:curator:Rahikainen') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 0) - - def test_read_catalog_record_search_by_curator_and_state_1(self): - response = self.client.get('/rest/v2/datasets?curator=id:of:curator:rahikainen&preservation_state=10') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 2) - self.assertEqual(response.data['results'][0]['id'], 2) - self.assertEqual(response.data['results'][0]['preservation_state'], 10) - self.assertEqual(response.data['results'][0]['research_dataset']['curator'][0]['name'], 'Rahikainen', - 'Curator name is not matching') - - def test_read_catalog_record_search_by_curator_and_state_2(self): - response = self.client.get('/rest/v2/datasets?curator=id:of:curator:rahikainen&preservation_state=40') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) - self.assertEqual(response.data['results'][0]['id'], 4) - self.assertEqual(response.data['results'][0]['preservation_state'], 40) - self.assertEqual(response.data['results'][0]['research_dataset']['curator'][0]['name'], 'Rahikainen', - 'Curator name is not matching') - - def test_read_catalog_record_search_by_curator_and_state_not_found(self): - response = self.client.get('/rest/v2/datasets?curator=id:of:curator:rahikainen&preservation_state=55') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 0) - - def test_read_catalog_record_search_by_creator_id(self): - cr = CatalogRecordV2.objects.get(pk=1) - cr.user_created = '123' - cr.force_save() - response = self.client.get('/rest/v2/datasets?user_created=123') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) - self.assertEqual(response.data['results'][0]['user_created'], '123') - - def test_read_catalog_record_search_by_metadata_provider_user(self): - response = self.client.get('/rest/v2/datasets?metadata_provider_user=123') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 0) - - cr = CatalogRecordV2.objects.get(pk=1) - cr.metadata_provider_user = '123' - cr.force_save() - - response = self.client.get('/rest/v2/datasets?metadata_provider_user=123') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 1) - - def test_read_catalog_record_search_by_metadata_owner_org(self): - owner_org = 'org_id' - for cr in CatalogRecordV2.objects.filter(pk__in=[1, 2, 3]): - cr.metadata_owner_org = owner_org - cr.force_save() - - owner_org_2 = 'org_id_2' - for cr in CatalogRecordV2.objects.filter(pk__in=[4, 5, 6]): - cr.metadata_owner_org = owner_org_2 - cr.force_save() - - response = self.client.get('/rest/v2/datasets?metadata_owner_org=%s' % owner_org) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 3) - - response = self.client.get('/rest/v2/datasets?metadata_owner_org=%s,%s' % (owner_org, owner_org_2)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 6) - - def test_filter_by_contract_org_identifier(self): - """ - Test filtering by contract_org_identifier, which matches using iregex - """ - metax_user = settings.API_METAX_USER - self._use_http_authorization(username=metax_user['username'], password=metax_user['password']) - - response = self.client.get('/rest/v2/datasets?contract_org_identifier=2345') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 10) - - response = self.client.get('/rest/v2/datasets?contract_org_identifier=1234567-1') - self.assertEqual(len(response.data['results']), 10) - - response = self.client.get('/rest/v2/datasets?contract_org_identifier=1234567-123') - self.assertEqual(len(response.data['results']), 0) - - def test_filter_by_contract_org_identifier_is_restricted(self): - response = self.client.get('/rest/v2/datasets?contract_org_identifier=1234') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_read_catalog_record_search_by_data_catalog_id(self): - from metax_api.models.data_catalog import DataCatalog - - # Create a new data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) - dc_id = 'original_dc_identifier' - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/v2/datacatalogs', dc, format="json") - - # Set the new data catalog for a catalog record and store the catalog record - cr = CatalogRecordV2.objects.get(pk=1) - cr.data_catalog = DataCatalog.objects.get(catalog_json__identifier=dc_id) - cr.force_save() - - # Verify - response = self.client.get('/rest/v2/datasets?data_catalog={0}'.format(dc_id)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1) - self.assertEqual(response.data['results'][0]['data_catalog']['identifier'], dc_id) - - def test_filter_by_deprecated(self): - cr = CatalogRecordV2.objects.get(pk=1) - cr.deprecated = True - cr.force_save() - - response = self.client.get('/rest/v2/datasets?deprecated=true') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 1, response.data['results']) - self.assertTrue(response.data['results'][0]['deprecated'], response.data) - - response = self.client.get('/rest/v2/datasets?deprecated=false') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['results'][0]['id'], 2, response.data) - self.assertFalse(response.data['results'][0]['deprecated'], response.data) - - response = self.client.get('/rest/v2/datasets?deprecated=badbool') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_filter_by_api_version(self): - - # update one dataset to v2 so that we get non-zero values for that as well - response = self.client.put('/rest/v2/datasets', [self.cr_from_test_data], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # both models return correct count because v2 model is just a proxy - v1_count = CatalogRecordV2.objects.filter(api_meta__contains={'version': 1}).count() - - response = self.client.get('/rest/v2/datasets?api_version=1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(v1_count, response.data['count'], response.data) - - v2_count = CatalogRecordV2.objects.filter(api_meta__contains={'version': 2}).count() - - response = self.client.get('/rest/v2/datasets?api_version=2') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(v2_count, response.data['count'], response.data) - - def test_filter_by_legacy(self): - self.create_legacy_data_catalogs() - self.create_legacy_dataset() - - # by default, legacy datasets are excluded - non_legacy_count = CatalogRecordV2.objects.exclude( - data_catalog__catalog_json__identifier__in=settings.LEGACY_CATALOGS - ).count() - response = self.client.get('/rest/v2/datasets') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(non_legacy_count, response.data['count'], response.data) - - # legacy datasets can be included with a parameter - count_all = CatalogRecordV2.objects.count() - response = self.client.get('/rest/v2/datasets?include_legacy') self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(count_all, response.data['count'], response.data) - -class CatalogRecordApiReadXMLTransformationTests(CatalogRecordApiReadCommon): - - """ - dataset xml transformations - """ - def _create_dataset_with_doi(self): - # Create ida data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) - dc_id = settings.IDA_DATA_CATALOG_IDENTIFIER - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/v2/datacatalogs', dc, format="json") + self.assertEqual(len(response.data["research_dataset"]["files"]), file_amt) + self.assertEqual(len(response.data["research_dataset"]["directories"]), dir_amt) - # Create new cr by requesting a doi identifier - cr_json = self.client.get('/rest/v2/datasets/1').data - cr_json.pop('preservation_identifier', None) - cr_json.pop('identifier') - cr_json['research_dataset'].pop('preferred_identifier', None) - cr_json['research_dataset']['publisher'] = {'@type': 'Organization', 'name': {'und': 'Testaaja'}} - cr_json['research_dataset']['issued'] = '2010-01-01' - cr_json['data_catalog'] = dc_id - response = self.client.post('/rest/v2/datasets?pid_type=doi', cr_json, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - return response.data - - def test_read_dataset_xml_format_metax(self): - response = self.client.get('/rest/v2/datasets/1?dataset_format=metax') - self._check_dataset_xml_format_response(response, '%s' % doi[len('doi:'):] in response.data, - True, response.data) - - def test_read_dataset_format_datacite_odd_lang_abbrevation(self): - cr = CatalogRecordV2.objects.get(pk=1) - cr.research_dataset['publisher'] = {'@type': 'Organization', 'name': {'zk': 'Testiorganisaatio'}} - cr.force_save() - response = self.client.get('/rest/v2/datasets/1?dataset_format=fairdata_datacite') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_read_dataset_format_dummy_datacite_doi(self): - """ - Ensure query parameter ?dummy_doi=true returns datacite xml with identifierType=DOI - and identifier value prefixed with 10.0/. If a real DOI is - available in the dataset, then dummy should NOT be returned. - """ - pid = self.client.get('/rest/v2/datasets/12').data['research_dataset']['preferred_identifier'] - self.assertEqual(pid.startswith('doi:'), False, pid) - - for dataset_format in ['datacite', 'fairdata_datacite']: - response = self.client.get('/rest/v2/datasets/12?dataset_format=%s&dummy_doi=true' % dataset_format) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('' in response.data, True, response.data) - self.assertEqual('10.0/%s' % pid in response.data, True, response.data) - - # ensure if a real doi exists, then dummy should never be returned - cr = self._create_dataset_with_doi() - response = self.client.get('/rest/v2/datasets/%d?dataset_format=datacite&dummy_doi=true' % cr['id']) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('' in response.data, True, response.data) - self.assertEqual(cr['preservation_identifier'][len('doi:'):] in response.data, True, response.data) - self.assertEqual('10.0/%s' % pid in response.data, False, response.data) - - def _check_dataset_xml_format_response(self, response, element_name): - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('content-type' in response._headers, True, response._headers) - self.assertEqual('application/xml' in response._headers['content-type'][1], True, response._headers) - self.assertEqual(' 6) + for f in response.data["research_dataset"]["files"]: + self.assertTrue("details" in f) + # The below assert is a bit arbitrary + self.assertFalse("identifier" in f) + for d in response.data["research_dataset"]["directories"]: + self.assertTrue("details" in d) + # The below assert is a bit arbitrary + self.assertFalse("identifier" in d) class CatalogRecordApiReadPopulateFileInfoTests(CatalogRecordApiReadCommon): @@ -799,38 +103,6 @@ class CatalogRecordApiReadPopulateFileInfoTests(CatalogRecordApiReadCommon): corresponding objects from their db tables. """ - def test_file_details_populated(self): - # without the flag nothing should happen - response = self.client.get('/rest/v2/datasets/1?include_user_metadata') - self.assertEqual(all('details' not in f for f in response.data['research_dataset']['files']), True) - - response = self.client.get('/rest/v2/datasets/1?include_user_metadata&file_details') - self.assertEqual(response.status_code, status.HTTP_200_OK) - # check all fiels have the extra key 'details', and all details have the key 'identifier'. - # presumably the details were then filled in. - self.assertEqual(all('details' in f for f in response.data['research_dataset']['files']), True) - self.assertEqual(all('identifier' in f['details'] for f in response.data['research_dataset']['files']), True) - - def test_directory_details_populated(self): - # id 11 is one of the example datasets with full details. they should have a couple - # of directories attached. - CatalogRecordV2.objects.get(pk=11).calculate_directory_byte_sizes_and_file_counts() - - response = self.client.get('/rest/v2/datasets/11?include_user_metadata&file_details') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - # check all dirs have the extra key 'details', and all details have the key 'identifier'. - # presumably the details were then filled in. - self.assertEqual(all('details' in f for f in response.data['research_dataset']['directories']), True) - self.assertEqual(all('identifier' in f['details'] for f in response.data['research_dataset']['directories']), - True) - - # additionally check that file counts and total byte sizes are as expected - self.assertEqual(response.data['research_dataset']['directories'][0]['details']['byte_size'], 21000) - self.assertEqual(response.data['research_dataset']['directories'][1]['details']['byte_size'], 21000) - self.assertEqual(response.data['research_dataset']['directories'][0]['details']['file_count'], 20) - self.assertEqual(response.data['research_dataset']['directories'][1]['details']['file_count'], 20) - def test_file_details_for_deprecated_datasets(self): """ When a dataset is deprecated, it is possible that some of its directories no longer exist. @@ -842,133 +114,16 @@ def test_file_details_for_deprecated_datasets(self): cr = CatalogRecordV2.objects.get(pk=11) file_identifiers = File.objects.filter( - project_identifier=cr.files.all()[0].project_identifier).values_list('identifier', flat=True) - - response = self.client.delete('/rest/v2/files', data=file_identifiers, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - response = self.client.get('/rest/v2/datasets/11?include_user_metadata&file_details', format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - -class CatalogRecordApiReadPopulateFileInfoAuthorizationTests(CatalogRecordApiReadCommon): - """ - Test populating individual research_dataset.file and directory objects with their - corresponding objects from their db tables from authorization perspective. - """ - - # THE OK TESTS - - def test_returns_all_details_for_open_catalog_record_if_no_authorization(self): - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify all file and dir details info is returned for open cr /rest/v2/datasets/?file_details without - # authorization - self._assert_ok(open_cr_json, 'no') - - def test_returns_all_details_for_login_catalog_record_if_no_authorization(self): - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify all file and dir details info is returned for open cr /rest/v2/datasets/?file_details without - # authorization - self._assert_ok(open_cr_json, 'no') - - def test_returns_all_details_for_open_catalog_record_if_service_authorization(self): - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify all file and dir details info is returned for open cr /rest/v2/datasets/?file_details with - # service authorization - self._assert_ok(open_cr_json, 'service') - - @responses.activate - def test_returns_all_details_for_open_catalog_record_if_owner_authorization(self): - self.create_end_user_data_catalogs() - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify all file and dir details info is returned for open owner-owned - # cr /rest/v2/datasets/?file_details with owner authorization - self._assert_ok(open_cr_json, 'owner') - - def test_returns_all_details_for_restricted_catalog_record_if_service_authorization(self): - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify all file and dir details info is returned for restricted cr /rest/v2/datasets/?file_details with - # service authorization - self._assert_ok(restricted_cr_json, 'service') - - @responses.activate - def test_returns_all_details_for_restricted_catalog_record_if_owner_authorization(self): - self.create_end_user_data_catalogs() - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify all file and dir details info is returned for restricted owner-owned cr - # /rest/v2/datasets/?file_details with owner authorization - self._assert_ok(restricted_cr_json, 'owner') - - def test_returns_all_details_for_embargoed_catalog_record_if_available_reached_and_no_authorization(self): - available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify all file and dir details info is returned for embargoed cr /rest/v2/datasets/?file_details when - # embargo date has been reached without authorization - self._assert_ok(available_embargoed_cr_json, 'no') - - # THE FORBIDDEN TESTS - - def test_returns_limited_info_for_restricted_catalog_record_if_no_authorization(self): - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify limited file and dir info for restricted cr /rest/v2/datasets/?file_details without authorization - self._assert_limited_or_no_file_dir_info(restricted_cr_json, 'no') - - def test_returns_limited_info_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization(self): - not_available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(False) - - # Verify limited file and dir info for embargoed cr /rest/v2/datasets/?file_details when embargo date has - # not been reached without authorization - self._assert_limited_or_no_file_dir_info(not_available_embargoed_cr_json, 'no') - - def _assert_limited_or_no_file_dir_info(self, cr_json, credentials_type): - self._set_http_authorization(credentials_type) - - file_amt = len(cr_json['research_dataset']['files']) - dir_amt = len(cr_json['research_dataset']['directories']) - pk = cr_json['id'] + project_identifier=cr.files.all()[0].project_identifier + ).values_list("identifier", flat=True) - response = self.client.get('/rest/v2/datasets/{0}?include_user_metadata&file_details'.format(pk)) + response = self.client.delete("/rest/v2/files", data=file_identifiers, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['files']), file_amt) - self.assertEqual(len(response.data['research_dataset']['directories']), dir_amt) - - for f in response.data['research_dataset']['files']: - self.assertTrue('details' in f) - # The below assert is a bit arbitrary - self.assertTrue(len(f['details'].keys()) < 5) - for d in response.data['research_dataset']['directories']: - self.assertTrue('details' in d) - # The below assert is a bit arbitrary - self.assertTrue(len(d['details'].keys()) < 5) - - def _assert_ok(self, cr_json, credentials_type): - self._set_http_authorization(credentials_type) - - file_amt = len(cr_json['research_dataset']['files']) - dir_amt = len(cr_json['research_dataset']['directories']) - pk = cr_json['id'] - - response = self.client.get('/rest/v2/datasets/{0}?include_user_metadata&file_details'.format(pk)) + response = self.client.get( + "/rest/v2/datasets/11?include_user_metadata&file_details", format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['files']), file_amt) - self.assertEqual(len(response.data['research_dataset']['directories']), dir_amt) - - for f in response.data['research_dataset']['files']: - self.assertTrue('details' in f) - # The below assert is a bit arbitrary - self.assertTrue(len(f['details'].keys()) > 5) - for d in response.data['research_dataset']['directories']: - self.assertTrue('details' in d) - # The below assert is a bit arbitrary - self.assertTrue(len(d['details'].keys()) > 5) class CatalogRecordApiReadFiles(CatalogRecordApiReadCommon): @@ -977,29 +132,23 @@ class CatalogRecordApiReadFiles(CatalogRecordApiReadCommon): Test /datasets/pid/files api """ - def test_get_files(self): - file_count = CatalogRecordV2.objects.get(pk=1).files.count() - response = self.client.get('/rest/v2/datasets/1/files') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), file_count) - def test_get_files_specified_fields_only(self): """ Test use of query parameter ?file_fields=x,y,z """ - response = self.client.get('/rest/v2/datasets/1/files?file_fields=identifier,file_path') + response = self.client.get("/rest/v2/datasets/1/files?file_fields=identifier,file_path") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data[0].keys()), 2) - self.assertEqual('identifier' in response.data[0], True) - self.assertEqual('file_path' in response.data[0], True) + self.assertEqual("identifier" in response.data[0], True) + self.assertEqual("file_path" in response.data[0], True) def test_removed_query_param(self): """ Test use of query parameter removed_files=bool in /datasets/pid/files, which should return only deleted files. """ - response = self.client.get('/rest/v2/datasets/1/files') - file_ids_before = set([ f['id'] for f in response.data ]) + response = self.client.get("/rest/v2/datasets/1/files") + file_ids_before = set([f["id"] for f in response.data]) obj = File.objects.get(pk=1) obj.removed = True obj.force_save() @@ -1007,14 +156,14 @@ def test_removed_query_param(self): obj2.removed = True obj2.force_save() - response = self.client.get('/rest/v2/datasets/1/files') + response = self.client.get("/rest/v2/datasets/1/files") self.assertEqual(response.status_code, status.HTTP_200_OK, response.content) self.assertEqual(len(response.data), 0) - response = self.client.get('/rest/v2/datasets/1/files?removed_files=true') + response = self.client.get("/rest/v2/datasets/1/files?removed_files=true") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(len(response.data), len(file_ids_before)) - self.assertEqual(file_ids_before, set([ f['id'] for f in response.data ])) + self.assertEqual(file_ids_before, set([f["id"] for f in response.data])) class CatalogRecordApiReadFilesAuthorization(CatalogRecordApiReadCommon): @@ -1023,76 +172,21 @@ class CatalogRecordApiReadFilesAuthorization(CatalogRecordApiReadCommon): Test /datasets/pid/files api from authorization perspective """ - # THE OK TESTS - - def test_returns_ok_for_open_catalog_record_if_no_authorization(self): - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify open dataset /rest/v2/datasets//files returns all the files even without authorization - self._assert_ok(open_cr_json, 'no') - - def test_returns_ok_for_open_catalog_record_if_service_authorization(self): - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify open dataset /rest/v2/datasets//files returns all the files with service authorization - self._assert_ok(open_cr_json, 'service') - - @responses.activate - def test_returns_ok_for_open_catalog_record_if_owner_authorization(self): - self.create_end_user_data_catalogs() - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify open owner-owned dataset /rest/v2/datasets//files returns all the files with owner authorization - self._assert_ok(open_cr_json, 'owner') - - def test_returns_ok_for_restricted_catalog_record_if_service_authorization(self): - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify restricted dataset /rest/v2/datasets//files returns all the files with service authorization - self._assert_ok(restricted_cr_json, 'service') - - @responses.activate - def test_returns_ok_for_restricted_catalog_record_if_owner_authorization(self): - self.create_end_user_data_catalogs() - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify restricted owner-owned dataset /rest/v2/datasets//files returns all the files with - # owner authorization - self._assert_ok(restricted_cr_json, 'owner') - - def test_returns_ok_for_embargoed_catalog_record_if_available_reached_and_no_authorization(self): - available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify restricted dataset /rest/v2/datasets//files returns ok when embargo date has been reached without - # authorization - self._assert_ok(available_embargoed_cr_json, 'no') - # THE FORBIDDEN TESTS - def test_returns_forbidden_for_restricted_catalog_record_if_no_authorization(self): - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify restricted dataset /rest/v2/datasets//files returns forbidden without authorization - self._assert_forbidden(restricted_cr_json, 'no') - - def test_returns_forbidden_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization(self): - not_available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(False) + def test_returns_forbidden_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization( + self, + ): + not_available_embargoed_cr_json = ( + self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(False) + ) # Verify restricted dataset /rest/v2/datasets//files returns forbidden when embargo # date has not been reached - self._assert_forbidden(not_available_embargoed_cr_json, 'no') + self._assert_forbidden(not_available_embargoed_cr_json, "no") def _assert_forbidden(self, cr_json, credentials_type): - pk = cr_json['id'] - self._set_http_authorization(credentials_type) - response = self.client.get('/rest/v2/datasets/{0}/files'.format(pk)) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def _assert_ok(self, cr_json, credentials_type): - pk = cr_json['id'] + pk = cr_json["id"] self._set_http_authorization(credentials_type) - rd = cr_json['research_dataset'] - file_amt = len(rd['files']) + sum(int(d['details']['file_count']) for d in rd['directories']) - response = self.client.get('/rest/v2/datasets/{0}/files'.format(pk)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), file_amt) + response = self.client.get("/rest/v2/datasets/{0}/files".format(pk)) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/referencedata.py b/src/metax_api/tests/api/rest/v2/views/datasets/referencedata.py deleted file mode 100755 index 5032cb2b..00000000 --- a/src/metax_api/tests/api/rest/v2/views/datasets/referencedata.py +++ /dev/null @@ -1,412 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2020 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from copy import deepcopy - -from rest_framework import status - -from metax_api.services import ReferenceDataMixin as RDM -from metax_api.services.redis_cache_service import RedisClient - -from .write import CatalogRecordApiWriteCommon - - -class CatalogRecordApiWriteReferenceDataTests(CatalogRecordApiWriteCommon): - """ - Tests related to reference_data validation and dataset fields population - from reference_data, according to given uri or code as the value. - """ - - def test_organization_name_is_required(self): - """ - Organization 'name' field is not madatory in the schema, but that is only because it does - not make sense to end users when using an identifier from reference data, which will overwrite - the name anyway. - - If an organization identifier is used, which is not found in the reference data, and therefore - does not populate the name automatically, then the user is required to provide the name. - """ - - # simple case - cr = deepcopy(self.cr_full_ida_test_data) - cr['research_dataset']['curator'] = [{ - '@type': 'Organization', - 'identifier': 'not found!', - # no name! - }] - response = self.client.post('/rest/v2/datasets', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # a more complex case. ensure organizations are found from deep structures - cr = deepcopy(self.cr_full_ida_test_data) - org = cr['research_dataset']['provenance'][0]['was_associated_with'][0] - del org['name'] # should cause the error - org['@type'] = 'Organization' - org['identifier'] = 'not found!' - response = self.client.post('/rest/v2/datasets', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # try again. should be ok - org['identifier'] = 'http://uri.suomi.fi/codelist/fairdata/organization/code/10076' - response = self.client.post('/rest/v2/datasets', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - def test_catalog_record_reference_data_missing_ok(self): - """ - The API should attempt to reload the reference data if it is missing from - cache for whatever reason, and successfully finish the request - """ - cache = RedisClient() - cache.delete('reference_data') - self.assertEqual(cache.get('reference_data', master=True), None, - 'cache ref data should be missing after cache.delete()') - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - def test_missing_license_identifier_ok(self): - """ - Missing license identifier is ok if url is provided. - Works on att and ida datasets - """ - rd_ida = self.cr_full_ida_test_data['research_dataset'] - rd_ida['access_rights']['license'] = [{ - 'license': "http://a.very.nice.custom/url" - }] - response = self.client.post('/rest/v2/datasets', self.cr_full_ida_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(len(response.data['research_dataset']['access_rights']['license'][0]), 1, response.data) - - rd_att = self.cr_full_att_test_data['research_dataset'] - rd_att['access_rights']['license'] = [{ - 'license': "http://also.fine.custom/uri", - 'description': { - 'en': "This is very informative description of this custom license." - } - }] - rd_att['remote_resources'][0]['license'] = [{ - 'license': "http://cool.remote.uri", - 'description': { - 'en': "Proof that also remote licenses can be used with custom urls." - } - }] - response = self.client.post('/rest/v2/datasets', self.cr_full_att_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(len(response.data['research_dataset']['access_rights']['license'][0]), 2, response.data) - self.assertEqual(len(response.data['research_dataset']['remote_resources'][0]['license'][0]), 2, response.data) - - def test_create_catalog_record_with_invalid_reference_data(self): - rd_ida = self.cr_full_ida_test_data['research_dataset'] - rd_ida['theme'][0]['identifier'] = 'nonexisting' - rd_ida['field_of_science'][0]['identifier'] = 'nonexisting' - rd_ida['language'][0]['identifier'] = 'nonexisting' - rd_ida['access_rights']['access_type']['identifier'] = 'nonexisting' - rd_ida['access_rights']['license'][0]['identifier'] = 'nonexisting' - rd_ida['other_identifier'][0]['type']['identifier'] = 'nonexisting' - rd_ida['spatial'][0]['place_uri']['identifier'] = 'nonexisting' - rd_ida['files'][0]['file_type']['identifier'] = 'nonexisting' - rd_ida['files'][0]['use_category']['identifier'] = 'nonexisting' - rd_ida['infrastructure'][0]['identifier'] = 'nonexisting' - rd_ida['creator'][0]['contributor_role'][0]['identifier'] = 'nonexisting' - rd_ida['curator'][0]['contributor_type'][0]['identifier'] = 'nonexisting' - rd_ida['is_output_of'][0]['funder_type']['identifier'] = 'nonexisting' - rd_ida['directories'][0]['use_category']['identifier'] = 'nonexisting' - rd_ida['relation'][0]['relation_type']['identifier'] = 'nonexisting' - rd_ida['relation'][0]['entity']['type']['identifier'] = 'nonexisting' - rd_ida['provenance'][0]['lifecycle_event']['identifier'] = 'nonexisting' - rd_ida['provenance'][1]['preservation_event']['identifier'] = 'nonexisting' - rd_ida['provenance'][0]['event_outcome']['identifier'] = 'nonexisting' - response = self.client.post('/rest/v2/datasets', self.cr_full_ida_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('research_dataset' in response.data.keys(), True) - self.assertEqual(len(response.data['research_dataset']), 19) - - rd_att = self.cr_full_att_test_data['research_dataset'] - rd_att['remote_resources'][0]['license'][0]['identifier'] = 'nonexisting' - rd_att['remote_resources'][1]['resource_type']['identifier'] = 'nonexisting' - rd_att['remote_resources'][0]['use_category']['identifier'] = 'nonexisting' - response = self.client.post('/rest/v2/datasets', self.cr_full_att_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('research_dataset' in response.data.keys(), True) - self.assertEqual(len(response.data['research_dataset']), 3) - - def test_create_catalog_record_populate_fields_from_reference_data(self): - """ - 1) Insert codes from cached reference data to dataset identifier fields - that will be validated, and then populated - 2) Check that that the values in dataset identifier fields are changed from - codes to uris after a successful create - 3) Check that labels have also been copied to datasets to their approriate fields - """ - cache = RedisClient() - rf = RDM.get_reference_data(cache) - refdata = rf['reference_data'] - orgdata = rf['organization_data'] - refs = {} - - data_types = [ - 'access_type', - 'restriction_grounds', - 'field_of_science', - 'identifier_type', - 'keyword', - 'language', - 'license', - 'location', - 'resource_type', - 'file_type', - 'use_category', - 'research_infra', - 'contributor_role', - 'contributor_type', - 'funder_type', - 'relation_type', - 'lifecycle_event', - 'preservation_event', - 'event_outcome' - ] - - # the values in these selected entries will be used throghout the rest of the test case - for dtype in data_types: - if dtype == 'location': - entry = next((obj for obj in refdata[dtype] if obj.get('wkt', False)), None) - self.assertTrue(entry is not None) - else: - entry = refdata[dtype][1] - refs[dtype] = { - 'code': entry['code'], - 'uri': entry['uri'], - 'label': entry.get('label', None), - 'wkt': entry.get('wkt', None), - 'scheme': entry.get('scheme', None) - } - - refs['organization'] = { - 'uri': orgdata['organization'][0]['uri'], - 'code': orgdata['organization'][0]['code'], - 'label': orgdata['organization'][0]['label'] - } - - # replace the relations with objects that have only the identifier set with code as value, - # to easily check that label was populated (= that it appeared in the dataset after create) - # without knowing its original value from the generated test data - rd_ida = self.cr_full_ida_test_data['research_dataset'] - rd_ida['theme'][0] = {'identifier': refs['keyword']['code']} - rd_ida['field_of_science'][0] = {'identifier': refs['field_of_science']['code']} - rd_ida['language'][0] = {'identifier': refs['language']['code']} - rd_ida['access_rights']['access_type'] = {'identifier': refs['access_type']['code']} - rd_ida['access_rights']['restriction_grounds'][0] = {'identifier': refs['restriction_grounds']['code']} - rd_ida['access_rights']['license'][0] = {'identifier': refs['license']['code']} - rd_ida['other_identifier'][0]['type'] = {'identifier': refs['identifier_type']['code']} - rd_ida['spatial'][0]['place_uri'] = {'identifier': refs['location']['code']} - rd_ida['files'][0]['file_type'] = {'identifier': refs['file_type']['code']} - rd_ida['files'][0]['use_category'] = {'identifier': refs['use_category']['code']} - rd_ida['directories'][0]['use_category'] = {'identifier': refs['use_category']['code']} - rd_ida['infrastructure'][0] = {'identifier': refs['research_infra']['code']} - rd_ida['creator'][0]['contributor_role'][0] = {'identifier': refs['contributor_role']['code']} - rd_ida['curator'][0]['contributor_type'][0] = {'identifier': refs['contributor_type']['code']} - rd_ida['is_output_of'][0]['funder_type'] = {'identifier': refs['funder_type']['code']} - rd_ida['relation'][0]['relation_type'] = {'identifier': refs['relation_type']['code']} - rd_ida['relation'][0]['entity']['type'] = {'identifier': refs['resource_type']['code']} - rd_ida['provenance'][0]['lifecycle_event'] = {'identifier': refs['lifecycle_event']['code']} - rd_ida['provenance'][1]['preservation_event'] = {'identifier': refs['preservation_event']['code']} - rd_ida['provenance'][0]['event_outcome'] = {'identifier': refs['event_outcome']['code']} - - # these have other required fields, so only update the identifier with code - rd_ida['is_output_of'][0]['source_organization'][0]['identifier'] = refs['organization']['code'] - rd_ida['is_output_of'][0]['has_funding_agency'][0]['identifier'] = refs['organization']['code'] - rd_ida['other_identifier'][0]['provider']['identifier'] = refs['organization']['code'] - rd_ida['contributor'][0]['member_of']['identifier'] = refs['organization']['code'] - rd_ida['creator'][0]['member_of']['identifier'] = refs['organization']['code'] - rd_ida['curator'][0]['is_part_of']['identifier'] = refs['organization']['code'] - rd_ida['publisher']['is_part_of']['identifier'] = refs['organization']['code'] - rd_ida['rights_holder'][0]['is_part_of']['identifier'] = refs['organization']['code'] - - # Other type of reference data populations - orig_wkt_value = rd_ida['spatial'][0]['as_wkt'][0] - rd_ida['spatial'][0]['place_uri']['identifier'] = refs['location']['code'] - rd_ida['spatial'][1]['as_wkt'] = [] - rd_ida['spatial'][1]['place_uri']['identifier'] = refs['location']['code'] - - response = self.client.post( - '/rest/v2/datasets?include_user_metadata', self.cr_full_ida_test_data, format="json" - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('research_dataset' in response.data.keys(), True) - - new_rd_ida = response.data['research_dataset'] - self._assert_uri_copied_to_identifier(refs, new_rd_ida) - self._assert_label_copied_to_pref_label(refs, new_rd_ida) - self._assert_label_copied_to_title(refs, new_rd_ida) - self._assert_label_copied_to_name(refs, new_rd_ida) - - # Assert if spatial as_wkt field has been populated with a value from ref data which has wkt value having - # condition that the user has not given own coordinates in the as_wkt field - self.assertEqual(orig_wkt_value, new_rd_ida['spatial'][0]['as_wkt'][0]) - self.assertEqual(refs['location']['wkt'], new_rd_ida['spatial'][1]['as_wkt'][0]) - - # rd from att data catalog - rd_att = self.cr_full_att_test_data['research_dataset'] - rd_att['remote_resources'][1]['resource_type'] = {'identifier': refs['resource_type']['code']} - rd_att['remote_resources'][0]['use_category'] = {'identifier': refs['use_category']['code']} - rd_att['remote_resources'][0]['license'][0] = {'identifier': refs['license']['code']} - - # Assert remote resources related reference datas - response = self.client.post('/rest/v2/datasets', self.cr_full_att_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('research_dataset' in response.data.keys(), True) - new_rd_att = response.data['research_dataset'] - self._assert_att_remote_resource_items(refs, new_rd_att) - - def _assert_att_remote_resource_items(self, refs, new_rd): - self.assertEqual(refs['resource_type']['uri'], new_rd['remote_resources'][1]['resource_type']['identifier']) - self.assertEqual(refs['use_category']['uri'], new_rd['remote_resources'][0]['use_category']['identifier']) - self.assertEqual(refs['license']['uri'], new_rd['remote_resources'][0]['license'][0]['identifier']) - self.assertEqual(refs['resource_type']['label'], - new_rd['remote_resources'][1]['resource_type'].get('pref_label', None)) - self.assertEqual(refs['use_category']['label'], - new_rd['remote_resources'][0]['use_category'].get('pref_label', None)) - self.assertEqual(refs['license']['label'], new_rd['remote_resources'][0]['license'][0].get('title', None)) - - def _assert_uri_copied_to_identifier(self, refs, new_rd): - self.assertEqual(refs['keyword']['uri'], new_rd['theme'][0]['identifier']) - self.assertEqual(refs['field_of_science']['uri'], new_rd['field_of_science'][0]['identifier']) - self.assertEqual(refs['language']['uri'], new_rd['language'][0]['identifier']) - self.assertEqual(refs['access_type']['uri'], new_rd['access_rights']['access_type']['identifier']) - self.assertEqual(refs['restriction_grounds']['uri'], - new_rd['access_rights']['restriction_grounds'][0]['identifier']) - self.assertEqual(refs['license']['uri'], new_rd['access_rights']['license'][0]['identifier']) - self.assertEqual(refs['identifier_type']['uri'], new_rd['other_identifier'][0]['type']['identifier']) - self.assertEqual(refs['location']['uri'], new_rd['spatial'][0]['place_uri']['identifier']) - self.assertEqual(refs['file_type']['uri'], new_rd['files'][0]['file_type']['identifier']) - self.assertEqual(refs['use_category']['uri'], new_rd['files'][0]['use_category']['identifier']) - - self.assertEqual(refs['use_category']['uri'], new_rd['directories'][0]['use_category']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['is_output_of'][0]['source_organization'][0]['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['is_output_of'][0]['has_funding_agency'][0]['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['other_identifier'][0]['provider']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['contributor'][0]['member_of']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['creator'][0]['member_of']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['curator'][0]['is_part_of']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['publisher']['is_part_of']['identifier']) - self.assertEqual(refs['organization']['uri'], new_rd['rights_holder'][0]['is_part_of']['identifier']) - self.assertEqual(refs['research_infra']['uri'], new_rd['infrastructure'][0]['identifier']) - self.assertEqual(refs['contributor_role']['uri'], new_rd['creator'][0]['contributor_role'][0]['identifier']) - self.assertEqual(refs['contributor_type']['uri'], new_rd['curator'][0]['contributor_type'][0]['identifier']) - self.assertEqual(refs['funder_type']['uri'], new_rd['is_output_of'][0]['funder_type']['identifier']) - self.assertEqual(refs['relation_type']['uri'], new_rd['relation'][0]['relation_type']['identifier']) - self.assertEqual(refs['resource_type']['uri'], new_rd['relation'][0]['entity']['type']['identifier']) - self.assertEqual(refs['lifecycle_event']['uri'], new_rd['provenance'][0]['lifecycle_event']['identifier']) - self.assertEqual(refs['preservation_event']['uri'], new_rd['provenance'][1]['preservation_event']['identifier']) - self.assertEqual(refs['event_outcome']['uri'], new_rd['provenance'][0]['event_outcome']['identifier']) - - def _assert_scheme_copied_to_in_scheme(self, refs, new_rd): - self.assertEqual(refs['keyword']['scheme'], new_rd['theme'][0]['in_scheme']) - self.assertEqual(refs['field_of_science']['scheme'], new_rd['field_of_science'][0]['in_scheme']) - self.assertEqual(refs['language']['scheme'], new_rd['language'][0]['in_scheme']) - self.assertEqual(refs['access_type']['scheme'], new_rd['access_rights']['access_type']['in_scheme']) - self.assertEqual(refs['restriction_grounds']['scheme'], - new_rd['access_rights']['restriction_grounds'][0]['in_scheme']) - self.assertEqual(refs['license']['scheme'], new_rd['access_rights']['license'][0]['in_scheme']) - self.assertEqual(refs['identifier_type']['scheme'], new_rd['other_identifier'][0]['type']['in_scheme']) - self.assertEqual(refs['location']['scheme'], new_rd['spatial'][0]['place_uri']['in_scheme']) - self.assertEqual(refs['file_type']['scheme'], new_rd['files'][0]['file_type']['in_scheme']) - self.assertEqual(refs['use_category']['scheme'], new_rd['files'][0]['use_category']['in_scheme']) - - self.assertEqual(refs['use_category']['scheme'], new_rd['directories'][0]['use_category']['in_scheme']) - self.assertEqual(refs['research_infra']['scheme'], new_rd['infrastructure'][0]['in_scheme']) - self.assertEqual(refs['contributor_role']['scheme'], new_rd['creator'][0]['contributor_role']['in_scheme']) - self.assertEqual(refs['contributor_type']['scheme'], new_rd['curator'][0]['contributor_type']['in_scheme']) - self.assertEqual(refs['funder_type']['scheme'], new_rd['is_output_of'][0]['funder_type']['in_scheme']) - self.assertEqual(refs['relation_type']['scheme'], new_rd['relation'][0]['relation_type']['in_scheme']) - self.assertEqual(refs['resource_type']['scheme'], new_rd['relation'][0]['entity']['type']['in_scheme']) - self.assertEqual(refs['lifecycle_event']['scheme'], new_rd['provenance'][0]['lifecycle_event']['in_scheme']) - self.assertEqual(refs['preservation_event']['scheme'], - new_rd['provenance'][1]['preservation_event']['in_scheme']) - self.assertEqual(refs['event_outcome']['scheme'], new_rd['provenance'][0]['event_outcome']['in_scheme']) - - def _assert_label_copied_to_pref_label(self, refs, new_rd): - self.assertEqual(refs['keyword']['label'], new_rd['theme'][0].get('pref_label', None)) - self.assertEqual(refs['field_of_science']['label'], new_rd['field_of_science'][0].get('pref_label', None)) - self.assertEqual(refs['access_type']['label'], new_rd['access_rights']['access_type'].get('pref_label', None)) - self.assertEqual(refs['restriction_grounds']['label'], - new_rd['access_rights']['restriction_grounds'][0].get('pref_label', None)) - self.assertEqual(refs['identifier_type']['label'], - new_rd['other_identifier'][0]['type'].get('pref_label', None)) - self.assertEqual(refs['location']['label'], new_rd['spatial'][0]['place_uri'].get('pref_label', None)) - self.assertEqual(refs['file_type']['label'], new_rd['files'][0]['file_type'].get('pref_label', None)) - self.assertEqual(refs['use_category']['label'], new_rd['files'][0]['use_category'].get('pref_label', None)) - self.assertEqual(refs['use_category']['label'], - new_rd['directories'][0]['use_category'].get('pref_label', None)) - - self.assertEqual(refs['research_infra']['label'], new_rd['infrastructure'][0].get('pref_label', None)) - self.assertEqual(refs['contributor_role']['label'], - new_rd['creator'][0]['contributor_role'][0].get('pref_label', None)) - self.assertEqual(refs['contributor_type']['label'], - new_rd['curator'][0]['contributor_type'][0].get('pref_label', None)) - self.assertEqual(refs['funder_type']['label'], new_rd['is_output_of'][0]['funder_type'].get('pref_label', None)) - self.assertEqual(refs['relation_type']['label'], new_rd['relation'][0]['relation_type'].get('pref_label', None)) - self.assertEqual(refs['resource_type']['label'], - new_rd['relation'][0]['entity']['type'].get('pref_label', None)) - self.assertEqual(refs['lifecycle_event']['label'], - new_rd['provenance'][0]['lifecycle_event'].get('pref_label', None)) - self.assertEqual(refs['preservation_event']['label'], - new_rd['provenance'][1]['preservation_event'].get('pref_label', None)) - self.assertEqual(refs['event_outcome']['label'], - new_rd['provenance'][0]['event_outcome'].get('pref_label', None)) - - def _assert_label_copied_to_title(self, refs, new_rd): - required_langs = dict((lang, val) for lang, val in refs['language']['label'].items() - if lang in ['fi', 'sv', 'en', 'und']) - self.assertEqual(required_langs, new_rd['language'][0].get('title', None)) - self.assertEqual(refs['license']['label'], new_rd['access_rights']['license'][0].get('title', None)) - - def _assert_label_copied_to_name(self, refs, new_rd): - self.assertEqual(refs['organization']['label'], - new_rd['is_output_of'][0]['source_organization'][0].get('name', None)) - self.assertEqual(refs['organization']['label'], - new_rd['is_output_of'][0]['has_funding_agency'][0].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['other_identifier'][0]['provider'].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['contributor'][0]['member_of'].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['creator'][0]['member_of'].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['curator'][0]['is_part_of'].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['publisher']['is_part_of'].get('name', None)) - self.assertEqual(refs['organization']['label'], new_rd['rights_holder'][0]['is_part_of'].get('name', None)) - - def test_refdata_sub_org_main_org_population(self): - # Test parent org gets populated when sub org is from ref data and user has not provided is_part_of relation - self.cr_test_data['research_dataset']['publisher'] = {'@type': 'Organization', 'identifier': '10076-A800'} - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('is_part_of' in response.data['research_dataset']['publisher'], True) - self.assertEqual('http://uri.suomi.fi/codelist/fairdata/organization/code/10076', - response.data['research_dataset']['publisher']['is_part_of']['identifier']) - self.assertTrue(response.data['research_dataset']['publisher']['is_part_of'].get('name', False)) - - # Test parent org does not get populated when sub org is from ref data and user has provided is_part_of relation - self.cr_test_data['research_dataset']['publisher'] = { - '@type': 'Organization', - 'identifier': '10076-A800', - 'is_part_of': { - '@type': 'Organization', - 'identifier': 'test_id', - 'name': {'und': 'test_name'} - }} - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('is_part_of' in response.data['research_dataset']['publisher'], True) - self.assertEqual('test_id', response.data['research_dataset']['publisher']['is_part_of']['identifier']) - self.assertEqual('test_name', response.data['research_dataset']['publisher']['is_part_of']['name']['und']) - - # Test nothing happens when org is a parent org - self.cr_test_data['research_dataset']['publisher'] = {'@type': 'Organization', 'identifier': '10076'} - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('is_part_of' not in response.data['research_dataset']['publisher'], True) diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/rems.py b/src/metax_api/tests/api/rest/v2/views/datasets/rems.py deleted file mode 100755 index 5e412023..00000000 --- a/src/metax_api/tests/api/rest/v2/views/datasets/rems.py +++ /dev/null @@ -1,656 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2020 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -import unittest -from copy import deepcopy - -import responses -from django.conf import settings as django_settings -from rest_framework import status - -from metax_api.services import RedisCacheService as cache, ReferenceDataMixin as RDM -from metax_api.tests.utils import get_test_oidc_token - -from .write import CatalogRecordApiWriteCommon - -IDA_CATALOG = django_settings.IDA_DATA_CATALOG_IDENTIFIER - - -@unittest.skipIf(django_settings.REMS['ENABLED'] is not True, 'Only run if REMS is enabled') -class CatalogRecordApiWriteREMS(CatalogRecordApiWriteCommon): - - rf = RDM.get_reference_data(cache) - # get by code to prevent failures if list ordering changes - access_permit = [type for type in rf['reference_data']['access_type'] if type['code'] == 'permit'][0] - access_open = [type for type in rf['reference_data']['access_type'] if type['code'] == 'open'][0] - - permit_rights = { - # license type does not matter - "license": [ - { - "title": rf['reference_data']['license'][0]['label'], - "identifier": rf['reference_data']['license'][0]['uri'] - } - ], - "access_type": { - "in_scheme": access_permit['scheme'], - "identifier": access_permit['uri'], - "pref_label": access_permit['label'] - } - } - - open_rights = { - "access_type": { - "in_scheme": access_open['scheme'], - "identifier": access_open['uri'], - "pref_label": access_open['label'] - } - } - - # any other than what is included in permit_rights is sufficient - other_license = rf['reference_data']['license'][1] - - def setUp(self): - super().setUp() - # Create ida data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) - dc_id = IDA_CATALOG - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/v2/datacatalogs', dc, format="json") - - # token for end user access - self.token = get_test_oidc_token(new_proxy=True) - - # mock successful rems access for creation, add fails later if needed. - # Not using regex to allow individual access failures - for entity in ['user', 'workflow', 'license', 'resource', 'catalogue-item']: - self._mock_rems_write_access_succeeds('POST', entity, 'create') - - self._mock_rems_read_access_succeeds('license') - - # mock successful rems access for deletion. Add fails later - for entity in ['catalogue-item', 'workflow', 'resource']: - self._mock_rems_write_access_succeeds(method='PUT', entity=entity, action='archived') - self._mock_rems_write_access_succeeds(method='PUT', entity=entity, action='enabled') - - self._mock_rems_read_access_succeeds('catalogue-item') - self._mock_rems_read_access_succeeds('application') - self._mock_rems_write_access_succeeds(method='POST', entity='application', action='close') - - responses.add( - responses.GET, - f"{django_settings.REMS['BASE_URL']}/health", - json={'healthy': True}, - status=200 - ) - - def _get_access_granter(self, malformed=False): - """ - Returns user information - """ - access_granter = { - "userid": "testcaseuser" if not malformed else 1234, - "name": "Test User", - "email": "testcase@user.com" - } - - return access_granter - - def _mock_rems_write_access_succeeds(self, method, entity, action): - """ - method: HTTP method to be mocked [PUT, POST] - entity: REMS entity [application, catalogue-item, license, resource, user, workflow] - action: Action taken to entity [archived, close, create, edit, enabled] - """ - req_type = responses.POST if method == 'POST' else responses.PUT - - body = {"success": True} - - if method == 'POST' and action != 'close': - # action condition needed because applications are closed with POST method - body['id'] = 6 - - responses.add( - req_type, - f"{django_settings.REMS['BASE_URL']}/{entity}s/{action}", - json=body, - status=200 - ) - - def _mock_rems_read_access_succeeds(self, entity): - if entity == 'license': - resp = [ - { - "id": 7, - "licensetype": "link", - "enabled": True, - "archived": False, - "localizations": { - "fi": { - "title": self.rf['reference_data']['license'][0]['label']['fi'], - "textcontent": self.rf['reference_data']['license'][0]['uri'] - }, - "und": { - "title": self.rf['reference_data']['license'][0]['label']['und'], - "textcontent": self.rf['reference_data']['license'][0]['uri'] - } - } - }, - { - "id": 8, - "licensetype": "link", - "enabled": True, - "archived": False, - "localizations": { - "en": { - "title": self.rf['reference_data']['license'][1]['label']['en'], - "textcontent": self.rf['reference_data']['license'][1]['uri'] - } - } - } - ] - - elif entity == 'catalogue-item': - resp = [ - { - "archived": False, - "localizations": { - "en": { - "id": 18, - "langcode": "en", - "title": "Removal test", - "infourl": "https://url.to.etsin.fi" - } - }, - "resource-id": 19, - "start": "2020-01-02T14:06:13.496Z", - "wfid": 15, - "resid": "preferred identifier", - "formid": 3, - "id": 18, - "expired": False, - "end": None, - "enabled": True - } - ] - - elif entity == 'application': - # only mock relevant data - resp = [ - { - 'application/workflow': { - 'workflow.dynamic/handlers': [ - { - 'userid': 'somehandler' - } - ] - }, - "application/id": 3, - 'application/applicant': { - 'userid': 'someapplicant' - }, - "application/resources": [ - { - "catalogue-item/title": { - "en": "Removal test" - }, - "resource/ext-id": "some:pref:id", - "catalogue-item/id": 5 - } - ], - "application/state": 'application.state/draft' - }, - { - 'application/workflow': { - 'workflow.dynamic/handlers': [ - { - 'userid': 'someid' - } - ] - }, - "application/id": 2, - 'application/applicant': { - 'userid': 'someotherapplicant' - }, - "application/resources": [ - { - "catalogue-item/title": { - "en": "Removal test" - }, - "resource/ext-id": "some:pref:id", - "catalogue-item/id": 5 - } - ], - "application/state": 'application.state/approved' - }, - { - 'application/workflow': { - 'workflow.dynamic/handlers': [ - { - 'userid': 'remsuid' - } - ] - }, - "application/id": 1, - 'application/applicant': { - 'userid': 'someapplicant' - }, - "application/resources": [ - { - "catalogue-item/title": { - "en": "Removal test" - }, - "resource/ext-id": 'Same:title:with:different:catalogue:item', - "catalogue-item/id": 18 - } - ], - "application/state": 'application.state/draft' - } - ] - - responses.add( - responses.GET, - f"{django_settings.REMS['BASE_URL']}/{entity}s", - json=resp, - status=200 - ) - - def _mock_rems_access_return_403(self, method, entity, action=''): - """ - Works also for GET method since failure responses from rems are identical for write and read operations - """ - req_type = responses.POST if method == 'POST' else responses.PUT if method == 'PUT' else responses.GET - - responses.replace( - req_type, - f"{django_settings.REMS['BASE_URL']}/{entity}s/{action}", - status=403 # anything else than 200 is a fail - ) - - def _mock_rems_access_return_error(self, method, entity, action=''): - """ - operation status is defined in the body so 200 response can also be failure. - """ - req_type = responses.POST if method == 'POST' else responses.PUT if method == 'PUT' else responses.GET - - errors = [ - { - "type": "some kind of identifier of this error", - "somedetail": "entity identifier the error is conserning" - } - ] - - responses.replace( - req_type, - f"{django_settings.REMS['BASE_URL']}/{entity}s/{action}", - json={"success": False, "errors": errors}, - status=200 - ) - - def _mock_rems_access_crashes(self, method, entity, action=''): - """ - Crash happens for example if there is a network error. Can be used for GET also - """ - req_type = responses.POST if method == 'POST' else responses.PUT if method == 'PUT' else responses.GET - - responses.replace( - req_type, - f"{django_settings.REMS['BASE_URL']}/{entity}s/{action}", - body=Exception('REMS_service should catch this one also') - ) - - def _create_new_rems_dataset(self): - """ - Modifies catalog record to be REMS managed and post it to Metax - """ - self.cr_test_data['research_dataset']['access_rights'] = self.permit_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG - self.cr_test_data['access_granter'] = self._get_access_granter() - - response = self.client.post('/rest/v2/datasets?include_user_metadata', self.cr_test_data, format="json") - - return response - - @responses.activate - def test_creating_permit_dataset_creates_catalogue_item_service_succeeds(self): - """ - Tests that catalogue item in REMS is created correctly on permit dataset creation - """ - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue(response.data.get('rems_identifier') is not None, 'rems_identifier should be present') - self.assertTrue(response.data.get('access_granter') is not None, 'access_granter should be present') - - @responses.activate - def test_creating_permit_dataset_creates_catalogue_item_service_fails_1(self): - """ - Test unsuccessful rems access - """ - self._mock_rems_access_return_403('POST', 'workflow', 'create') - - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) - self.assertTrue('failed to publish updates' in response.data['detail'][0], response.data) - - @responses.activate - def test_creating_permit_dataset_creates_catalogue_item_service_fails_2(self): - """ - Test unsuccessful rems access - """ - self._mock_rems_access_return_error('POST', 'catalogue-item', 'create') - - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) - - @responses.activate - def test_creating_permit_dataset_creates_catalogue_item_service_fails_3(self): - """ - Test unsuccessful rems access - """ - self._mock_rems_access_crashes('POST', 'resource', 'create') - - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) - self.assertTrue('failed to publish updates' in response.data['detail'][0], response.data) - - @responses.activate - def test_changing_dataset_to_permit_creates_new_catalogue_item_succeeds(self): - """ - Test that changing access type to permit invokes the REMS update - """ - - # create dataset without rems managed access - self.cr_test_data['research_dataset']['access_rights'] = self.open_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - # change to rems managed - cr = response.data - cr['research_dataset']['access_rights'] = self.permit_rights - cr['access_granter'] = self._get_access_granter() - - response = self.client.put(f'/rest/v2/datasets/{cr["id"]}', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data.get('rems_identifier') is not None, 'rems_identifier should be present') - self.assertTrue(response.data.get('access_granter') is not None, 'access_granter should be present') - - @responses.activate - def test_changing_dataset_to_permit_creates_new_catalogue_item_fails(self): - """ - Test error handling on metax update operation - """ - self._mock_rems_access_return_error('POST', 'user', 'create') - - # create dataset without rems managed access - self.cr_test_data['research_dataset']['access_rights'] = self.open_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - # change to rems managed - cr = response.data - cr['research_dataset']['access_rights'] = self.permit_rights - cr['access_granter'] = self._get_access_granter() - - response = self.client.put(f'/rest/v2/datasets/{cr["id"]}', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) - - @responses.activate - def test_changing_access_type_to_other_closes_rems_entities_succeeds(self): - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr = response.data - cr['research_dataset']['access_rights'] = self.open_rights - - response = self.client.put(f'/rest/v2/datasets/{cr["id"]}', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - @responses.activate - def test_changing_access_type_to_other_closes_rems_entities_fails(self): - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - self._mock_rems_access_return_error('POST', 'application', 'close') - - cr = response.data - cr['research_dataset']['access_rights'] = self.open_rights - - response = self.client.put(f'/rest/v2/datasets/{cr["id"]}', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) - - @responses.activate - def test_changing_dataset_license_updates_rems(self): - """ - Create REMS dataset and change it's license. Ensure that - request is successful and that dataset's rems_identifier is changed. - """ - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr_before = response.data - - rems_id_before = cr_before['rems_identifier'] - cr_before['research_dataset']['access_rights']['license'] = [ - { - "title": self.other_license['label'], - "identifier": self.other_license['uri'] - } - ] - - response = self.client.put(f'/rest/v2/datasets/{cr_before["id"]}', cr_before, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - cr_after = response.data - self.assertNotEqual(rems_id_before, cr_after['rems_identifier'], 'REMS identifier should have been changed') - - @responses.activate - def test_changing_license_dont_allow_access_granter_changes(self): - """ - Create REMS dataset and change it's license. Ensure that - request is successful and that dataset's access_granter is not changed. - """ - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr_before = response.data - - cr_before['access_granter']['userid'] = 'newid' - cr_before['research_dataset']['access_rights']['license'] = [ - { - "identifier": self.other_license['uri'] - } - ] - - response = self.client.put(f'/rest/v2/datasets/{cr_before["id"]}', cr_before, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - cr_after = response.data - self.assertNotEqual('newid', cr_after['access_granter']['userid'], 'userid should not have been changed') - - @responses.activate - def test_deleting_license_updates_rems(self): - """ - Create REMS dataset and delete it's license. Ensure that rems_identifier is removed and no failures occur. - """ - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr_before = response.data - - cr_before['research_dataset']['access_rights'].pop('license') - - response = self.client.put(f'/rest/v2/datasets/{cr_before["id"]}', cr_before, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - cr_after = response.data - self.assertTrue(cr_after.get('rems_identifier') is None, 'REMS identifier should have been deleted') - self.assertTrue(cr_after.get('access_granter') is None, 'access_granter should have been deleted') - - @responses.activate - def test_creating_permit_dataset_creates_catalogue_item_end_user(self): - """ - Tests that catalogue item in REMS is created correctly on permit dataset creation. - User information is fetch'd from token. - """ - self._set_http_authorization('owner') - - # modify catalog record - self.cr_test_data['user_created'] = self.token['CSCUserName'] - self.cr_test_data['metadata_provider_user'] = self.token['CSCUserName'] - self.cr_test_data['metadata_provider_org'] = self.token['schacHomeOrganization'] - self.cr_test_data['metadata_owner_org'] = self.token['schacHomeOrganization'] - self.cr_test_data['research_dataset']['access_rights'] = self.permit_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG - - # end user doesn't have permissions to the files and they are also not needed in this test - del self.cr_test_data['research_dataset']['files'] - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - @responses.activate - def test_deleting_permit_dataset_removes_catalogue_item_succeeds(self): - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr_id = response.data['id'] - - # delete dataset - response = self.client.delete(f'/rest/v2/datasets/{cr_id}') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - - cr = self.client.get(f'/rest/v2/datasets/{cr_id}?removed').data - self.assertTrue(cr.get('rems_identifier') is None, 'rems_identifier should not be present') - self.assertTrue(cr.get('access_granter') is None, 'access_granter should not be present') - - @responses.activate - def test_deleting_permit_dataset_removes_catalogue_item_fails(self): - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - # delete dataset - self._mock_rems_access_return_error('PUT', 'catalogue-item', 'enabled') - - response = self.client.delete(f'/rest/v2/datasets/{response.data["id"]}') - self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) - - @responses.activate - def test_deprecating_permit_dataset_removes_catalogue_item_succeeds(self): - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr_before = response.data - # deprecate dataset - response = self.client.delete(f"/rest/v2/files/{cr_before['research_dataset']['files'][0]['identifier']}") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - cr_after = self.client.get(f'/rest/v2/datasets/{cr_before["id"]}').data - self.assertTrue(cr_after.get('rems_identifier') is None, 'rems_identifier should not be present') - self.assertTrue(cr_after.get('access_granter') is None, 'access_granter should not be present') - - @responses.activate - def test_deprecating_permit_dataset_removes_catalogue_item_fails(self): - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - # deprecate dataset - self._mock_rems_access_crashes('PUT', 'workflow', 'archived') - - response = self.client.delete(f"/rest/v2/files/{response.data['research_dataset']['files'][0]['identifier']}") - self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE, response.data) - self.assertTrue('failed to publish' in response.data['detail'][0], response.data) - - def test_missing_access_granter(self): - """ - Access_granter field is required when dataset is made REMS managed and - user is service. - """ - - # test on create - self.cr_test_data['research_dataset']['access_rights'] = self.permit_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('access_granter' in response.data['detail'][0], response.data) - - # test on update - self.cr_test_data['research_dataset']['access_rights'] = self.open_rights - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr = response.data - cr['research_dataset']['access_rights'] = self.permit_rights - response = self.client.put(f'/rest/v2/datasets/{cr["id"]}', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('access_granter' in response.data['detail'][0], response.data) - - def test_bad_access_granter_parameter(self): - """ - Access_granter values must be strings - """ - self.cr_test_data['research_dataset']['access_rights'] = self.permit_rights - self.cr_test_data['data_catalog'] = IDA_CATALOG - self.cr_test_data['access_granter'] = self._get_access_granter(malformed=True) - - response = self.client.post( - '/rest/v2/datasets', - self.cr_test_data, - format="json" - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('must be string' in response.data['detail'][0], response.data) - - def test_missing_license_in_dataset(self): - """ - License is required when dataset is REMS managed - """ - self.cr_test_data['research_dataset']['access_rights'] = deepcopy(self.permit_rights) - del self.cr_test_data['research_dataset']['access_rights']['license'] - self.cr_test_data['data_catalog'] = IDA_CATALOG - - response = self.client.post( - f'/rest/v2/datasets?access_granter={self._get_access_granter()}', - self.cr_test_data, - format="json" - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('must define license' in response.data['detail'][0], response.data) - - @responses.activate - def test_only_return_rems_info_to_privileged(self): - self._set_http_authorization('service') - - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue(response.data.get('rems_identifier') is not None, 'rems_identifier should be returned to owner') - self.assertTrue(response.data.get('access_granter') is not None, 'access_granter should be returned to owner') - - self._set_http_authorization('no') - response = self.client.get(f'/rest/v2/datasets/{response.data["id"]}') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data.get('rems_identifier') is None, 'rems_identifier should not be returned to Anon') - self.assertTrue(response.data.get('access_granter') is None, 'access_granter should not be returned to Anon') - - @responses.activate - def test_rems_info_cannot_be_changed(self): - response = self._create_new_rems_dataset() - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr = response.data - - cr['rems_identifier'] = 'some:new:identifier' - cr['access_granter']['name'] = 'New Name' - - response = self.client.put(f'/rest/v2/datasets/{cr["id"]}', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertNotEqual(response.data['rems_identifier'], 'some:new:identifier', 'rems_id should not be changed') - self.assertNotEqual(response.data['access_granter'], 'New Name', 'access_granter should not be changed') diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/write.py b/src/metax_api/tests/api/rest/v2/views/datasets/write.py index 58bce4cb..43f3d012 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/write.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/write.py @@ -6,18 +6,14 @@ # :license: MIT from copy import deepcopy -from datetime import timedelta -import responses from django.conf import settings as django_settings from django.core.management import call_command from rest_framework import status from rest_framework.test import APITestCase -from metax_api.models import AlternateRecordSet, CatalogRecordV2, Contract, DataCatalog, File -from metax_api.models.catalog_record import ACCESS_TYPES -from metax_api.tests.utils import TestClassUtils, get_test_oidc_token, test_data_file_path -from metax_api.utils import get_tz_aware_now_without_micros +from metax_api.models import CatalogRecordV2, DataCatalog +from metax_api.tests.utils import TestClassUtils, test_data_file_path CR = CatalogRecordV2 @@ -38,19 +34,24 @@ def setUp(self): """ Reloaded for every test case """ - call_command('loaddata', test_data_file_path, verbosity=0) - catalog_record_from_test_data = self._get_object_from_test_data('catalogrecord') - self.preferred_identifier = catalog_record_from_test_data['research_dataset']['preferred_identifier'] - self.identifier = catalog_record_from_test_data['identifier'] - self.pk = catalog_record_from_test_data['id'] + call_command("loaddata", test_data_file_path, verbosity=0) + catalog_record_from_test_data = self._get_object_from_test_data("catalogrecord") + self.preferred_identifier = catalog_record_from_test_data["research_dataset"][ + "preferred_identifier" + ] + self.identifier = catalog_record_from_test_data["identifier"] + self.pk = catalog_record_from_test_data["id"] """ New data that is sent to the server for POST, PUT, PATCH requests. Modified slightly as approriate for different purposes """ self.cr_test_data = self._get_new_test_cr_data() - self.cr_test_data['research_dataset']['publisher'] = {'@type': 'Organization', 'name': {'und': 'Testaaja'}} - self.cr_test_data['research_dataset']['issued'] = '2010-01-01' + self.cr_test_data["research_dataset"]["publisher"] = { + "@type": "Organization", + "name": {"und": "Testaaja"}, + } + self.cr_test_data["research_dataset"]["issued"] = "2010-01-01" self.cr_att_test_data = self._get_new_test_cr_data(cr_index=14, dc_index=5) self.cr_test_data_new_identifier = self._get_new_test_cr_data_with_updated_identifier() @@ -59,15 +60,6 @@ def setUp(self): self._use_http_authorization() - def update_record(self, record): - return self.client.put('/rest/v2/datasets/%d' % record['id'], record, format="json") - - def get_next_version(self, record): - self.assertEqual('next_dataset_version' in record, True) - response = self.client.get('/rest/v2/datasets/%d' % record['next_dataset_version']['id'], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - return response.data - # # # @@ -77,45 +69,62 @@ def get_next_version(self, record): # def _get_new_test_cr_data(self, cr_index=0, dc_index=0, c_index=0): - dc = self._get_object_from_test_data('datacatalog', requested_index=dc_index) - catalog_record_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=cr_index) - - if dc['catalog_json']['research_dataset_schema'] == 'ida' and \ - 'remote_resources' in catalog_record_from_test_data['research_dataset']: - self.fail("Cannot generate the requested test catalog record since requested data catalog is indicates ida " - "schema and the requested catalog record is having remote resources, which is not allowed") - - if dc['catalog_json']['research_dataset_schema'] == 'att' and \ - ('files' in catalog_record_from_test_data['research_dataset'] or - 'directories' in catalog_record_from_test_data['research_dataset']): - self.fail("Cannot generate the requested test catalog record since requested data catalog is indicates att " - "schema and the requested catalog record is having files or directories, which is not allowed") - - catalog_record_from_test_data.update({ - "contract": self._get_object_from_test_data('contract', requested_index=c_index), - "data_catalog": dc - }) - catalog_record_from_test_data['research_dataset'].update({ - "creator": [{ - "@type": "Person", - "name": "Teppo Testaaja", - "member_of": { - "@type": "Organization", - "name": {"fi": "Mysterious Organization"} - } - }], - "curator": [{ - "@type": "Person", - "name": "Default Owner", - "member_of": { - "@type": "Organization", - "name": {"fi": "Mysterious Organization"} - } - }] - }) - catalog_record_from_test_data['research_dataset'].pop('preferred_identifier', None) - catalog_record_from_test_data['research_dataset'].pop('metadata_version_identifier', None) - catalog_record_from_test_data.pop('identifier', None) + dc = self._get_object_from_test_data("datacatalog", requested_index=dc_index) + catalog_record_from_test_data = self._get_object_from_test_data( + "catalogrecord", requested_index=cr_index + ) + + if ( + dc["catalog_json"]["research_dataset_schema"] == "ida" + and "remote_resources" in catalog_record_from_test_data["research_dataset"] + ): + self.fail( + "Cannot generate the requested test catalog record since requested data catalog is indicates ida " + "schema and the requested catalog record is having remote resources, which is not allowed" + ) + + if dc["catalog_json"]["research_dataset_schema"] == "att" and ( + "files" in catalog_record_from_test_data["research_dataset"] + or "directories" in catalog_record_from_test_data["research_dataset"] + ): + self.fail( + "Cannot generate the requested test catalog record since requested data catalog is indicates att " + "schema and the requested catalog record is having files or directories, which is not allowed" + ) + + catalog_record_from_test_data.update( + { + "contract": self._get_object_from_test_data("contract", requested_index=c_index), + "data_catalog": dc, + } + ) + catalog_record_from_test_data["research_dataset"].update( + { + "creator": [ + { + "@type": "Person", + "name": "Teppo Testaaja", + "member_of": { + "@type": "Organization", + "name": {"fi": "Mysterious Organization"}, + }, + } + ], + "curator": [ + { + "@type": "Person", + "name": "Default Owner", + "member_of": { + "@type": "Organization", + "name": {"fi": "Mysterious Organization"}, + }, + } + ], + } + ) + catalog_record_from_test_data["research_dataset"].pop("preferred_identifier", None) + catalog_record_from_test_data["research_dataset"].pop("metadata_version_identifier", None) + catalog_record_from_test_data.pop("identifier", None) return catalog_record_from_test_data def _get_new_test_cr_data_with_updated_identifier(self): @@ -129,26 +138,40 @@ def _get_new_full_test_ida_cr_data(self): """ Returns one of the fuller generated test datasets """ - catalog_record_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=11) - data_catalog_from_test_data = self._get_object_from_test_data('datacatalog', requested_index=0) - return self._get_new_full_test_cr_data(catalog_record_from_test_data, data_catalog_from_test_data) + catalog_record_from_test_data = self._get_object_from_test_data( + "catalogrecord", requested_index=11 + ) + data_catalog_from_test_data = self._get_object_from_test_data( + "datacatalog", requested_index=0 + ) + return self._get_new_full_test_cr_data( + catalog_record_from_test_data, data_catalog_from_test_data + ) def _get_new_full_test_att_cr_data(self): """ Returns one of the fuller generated test datasets """ - catalog_record_from_test_data = self._get_object_from_test_data('catalogrecord', requested_index=23) - data_catalog_from_test_data = self._get_object_from_test_data('datacatalog', requested_index=5) - return self._get_new_full_test_cr_data(catalog_record_from_test_data, data_catalog_from_test_data) + catalog_record_from_test_data = self._get_object_from_test_data( + "catalogrecord", requested_index=23 + ) + data_catalog_from_test_data = self._get_object_from_test_data( + "datacatalog", requested_index=5 + ) + return self._get_new_full_test_cr_data( + catalog_record_from_test_data, data_catalog_from_test_data + ) def _get_new_full_test_cr_data(self, cr_from_test_data, dc_from_test_data): - cr_from_test_data.update({ - "contract": self._get_object_from_test_data('contract', requested_index=0), - "data_catalog": dc_from_test_data - }) - cr_from_test_data['research_dataset'].pop('metadata_version_identifier') - cr_from_test_data['research_dataset'].pop('preferred_identifier') - cr_from_test_data.pop('identifier') + cr_from_test_data.update( + { + "contract": self._get_object_from_test_data("contract", requested_index=0), + "data_catalog": dc_from_test_data, + } + ) + cr_from_test_data["research_dataset"].pop("metadata_version_identifier") + cr_from_test_data["research_dataset"].pop("preferred_identifier") + cr_from_test_data.pop("identifier") return cr_from_test_data @@ -163,264 +186,53 @@ class CatalogRecordApiWriteCreateTests(CatalogRecordApiWriteCommon): def setUp(self): super().setUp() - def test_issued_date_is_generated(self): - ''' Issued date is generated for all but harvested catalogs if it doesn't exists ''' - dc = DataCatalog.objects.get(pk=2) - dc.catalog_json['identifier'] = IDA_CATALOG # Test with IDA catalog - dc.force_save() - - self.cr_test_data['data_catalog'] = dc.catalog_json - self.cr_test_data['research_dataset'].pop('issued', None) - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue('issued' in response.data['research_dataset'], response.data) - - def test_create_catalog_record(self): - self.cr_test_data['research_dataset']['preferred_identifier'] = 'this_should_be_overwritten' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('research_dataset' in response.data.keys(), True) - self.assertEqual('metadata_version_identifier' in response.data['research_dataset'], True, - 'metadata_version_identifier should have been generated') - self.assertEqual('preferred_identifier' in response.data['research_dataset'], True, - 'preferred_identifier should have been generated') - self.assertNotEqual( - self.cr_test_data['research_dataset']['preferred_identifier'], - response.data['research_dataset']['preferred_identifier'], - 'in fairdata catalogs, user is not allowed to set preferred_identifier' - ) - self.assertNotEqual( - response.data['research_dataset']['preferred_identifier'], - response.data['research_dataset']['metadata_version_identifier'], - 'preferred_identifier and metadata_version_identifier should be generated separately' - ) - cr = CatalogRecordV2.objects.get(pk=response.data['id']) - self.assertEqual(cr.date_created >= get_tz_aware_now_without_micros() - timedelta(seconds=5), True, - 'Timestamp should have been updated during object creation') - - def test_create_catalog_record_as_harvester(self): - self.cr_test_data['research_dataset']['preferred_identifier'] = 'this_should_be_saved' - self.cr_test_data['data_catalog'] = 3 - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual( - self.cr_test_data['research_dataset']['preferred_identifier'], - response.data['research_dataset']['preferred_identifier'], - 'in harvested catalogs, user (the harvester) is allowed to set preferred_identifier' - ) - - def test_preferred_identifier_is_checked_also_from_deleted_records(self): - """ - If a catalog record having a specific preferred identifier is deleted, and a new catalog - record is created having the same preferred identifier, metax should deny this request - since a catalog record with the same pref id already exists, albeit deleted. - """ - - # dc 3 happens to be harvested catalog, which allows setting pref id - cr = CatalogRecordV2.objects.filter(data_catalog_id=3).first() - response = self.client.delete('/rest/v2/datasets/%d' % cr.id) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - self.cr_test_data['research_dataset']['preferred_identifier'] = cr.preferred_identifier - self.cr_test_data['data_catalog'] = 3 - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('already exists' in response.data['research_dataset'][0], True, response.data) - - def test_create_catalog_contract_string_identifier(self): - contract_identifier = Contract.objects.first().contract_json['identifier'] - self.cr_test_data['contract'] = contract_identifier - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['contract']['identifier'], contract_identifier, response.data) - - def test_create_catalog_error_contract_string_identifier_not_found(self): - self.cr_test_data['contract'] = 'doesnotexist' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - # self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, 'Should have raised 404 not found') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('contract' in response.data, True, 'Error should have been about contract not found') - - def test_create_catalog_record_json_validation_error_1(self): - """ - Ensure the json path of the error is returned along with other details - """ - self.cr_test_data['research_dataset']["title"] = 1234456 - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(len(response.data), 2, 'there should be two errors (error_identifier is one of them)') - self.assertEqual('research_dataset' in response.data.keys(), True, - 'The error should concern the field research_dataset') - self.assertEqual('1234456 is not of type' in response.data['research_dataset'][0], True, response.data) - self.assertEqual('Json path: [\'title\']' in response.data['research_dataset'][0], True, response.data) - - def test_create_catalog_record_json_validation_error_2(self): - """ - Ensure the json path of the error is returned along with other details also in - objects that are deeply nested - """ - self.cr_test_data['research_dataset']['provenance'] = [{ - 'title': {'en': 'provenance title'}, - 'was_associated_with': [ - {'@type': 'Person', 'xname': 'seppo'} - ] - }] - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(len(response.data), 2, 'there should be two errors (error_identifier is one of them)') - self.assertEqual('research_dataset' in response.data.keys(), True, - 'The error should concern the field research_dataset') - self.assertEqual('is not valid' in response.data['research_dataset'][0], True, response.data) - self.assertEqual('was_associated_with' in response.data['research_dataset'][0], True, response.data) - - def test_create_catalog_record_allowed_projects_ok(self): - response = self.client.post('/rest/v2/datasets?allowed_projects=project_x', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - def test_create_catalog_record_allowed_projects_fail(self): - # dataset file not in allowed projects + def test_create_catalog_record_using_pid_type(self): + # Test with pid_type = urn + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" response = self.client.post( - '/rest/v2/datasets?allowed_projects=no,permission', self.cr_test_data, format="json" + "/rest/v2/datasets?pid_type=urn", self.cr_test_data, format="json" ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - # ensure list is properly handled (separated by comma, end result should be list) - response = self.client.post('/rest/v2/datasets?allowed_projects=no_good_project_x,another', - self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - # handle empty value - response = self.client.post('/rest/v2/datasets?allowed_projects=', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - # Other trickery - response = self.client.post('/rest/v2/datasets?allowed_projects=,', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - # - # create list operations - # - - def test_create_catalog_record_list(self): - response = self.client.post('/rest/v2/datasets', - [self.cr_test_data, self.cr_test_data_new_identifier], format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('object' in response.data['success'][0].keys(), True) - self.assertEqual(len(response.data['success']), 2) - self.assertEqual(len(response.data['failed']), 0) - - def test_create_catalog_record_list_error_one_fails(self): - self.cr_test_data['research_dataset']["title"] = 1234456 - response = self.client.post('/rest/v2/datasets', - [self.cr_test_data, self.cr_test_data_new_identifier], format="json") - - """ - List response looks like - { - 'success': [ - { 'object': object }, - more objects... - ], - 'failed': [ - { - 'object': object, - 'errors': {'field': ['message'], 'otherfiled': ['message']} - }, - more objects... - ] - } - """ - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('object' in response.data['failed'][0].keys(), True) - self.assertEqual('research_dataset' in response.data['failed'][0]['errors'], True, response.data) - self.assertEqual( - '1234456 is not of type' in response.data['failed'][0]['errors']['research_dataset'][0], - True, - response.data - ) - self.assertEqual( - 'Json path: [\'title\']' in response.data['failed'][0]['errors']['research_dataset'][0], - True, - response.data + self.assertTrue( + response.data["research_dataset"]["preferred_identifier"].startswith("urn:") ) - def test_create_catalog_record_list_error_all_fail(self): - # data catalog is a required field, should fail - self.cr_test_data['data_catalog'] = None - self.cr_test_data_new_identifier['data_catalog'] = None - - response = self.client.post('/rest/v2/datasets', - [self.cr_test_data, self.cr_test_data_new_identifier], format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('object' in response.data['failed'][0].keys(), True) - self.assertEqual(len(response.data['success']), 0) - self.assertEqual(len(response.data['failed']), 2) - - def test_parameter_migration_override_preferred_identifier_when_creating(self): - """ - Normally, when saving to att/ida catalogs, providing a custom preferred_identifier is not - permitted. Using the optional query parameter ?migration_override=bool a custom preferred_identifier - can be passed. - """ - custom_pid = 'custom-pid-value' - self.cr_test_data['research_dataset']['preferred_identifier'] = custom_pid - response = self.client.post('/rest/v2/datasets?migration_override', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], custom_pid) - - def test_parameter_migration_override_no_preferred_identifier_when_creating(self): - """ - Normally, when saving to att/ida catalogs, providing a custom preferred_identifier is not - permitted. Using the optional query parameter ?migration_override=bool a custom preferred_identifier - can be passed. - """ - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/v2/datasets?migration_override', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue(len(response.data['research_dataset']['preferred_identifier']) > 0) - - self.cr_test_data['research_dataset'].pop('preferred_identifier', None) - response = self.client.post('/rest/v2/datasets?migration_override', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue(len(response.data['research_dataset']['preferred_identifier']) > 0) - - def test_create_catalog_record_using_pid_type(self): - # Test with pid_type = urn - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/v2/datasets?pid_type=urn', self.cr_test_data, format="json") - self.assertTrue(response.data['research_dataset']['preferred_identifier'].startswith('urn:')) - # Test with pid_type = doi AND not ida catalog - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/v2/datasets?pid_type=doi', self.cr_test_data, format="json") + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post( + "/rest/v2/datasets?pid_type=doi", self.cr_test_data, format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # Create ida data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) + dc = self._get_object_from_test_data("datacatalog", requested_index=0) dc_id = IDA_CATALOG - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/v2/datacatalogs', dc, format="json") + dc["catalog_json"]["identifier"] = dc_id + self.client.post("/rest/v2/datacatalogs", dc, format="json") # Test with pid_type = doi AND ida catalog - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - self.cr_test_data['data_catalog'] = IDA_CATALOG - response = self.client.post('/rest/v2/datasets?pid_type=doi', self.cr_test_data, format="json") - self.assertTrue(response.data['research_dataset']['preferred_identifier'].startswith('doi:10.')) + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + self.cr_test_data["data_catalog"] = IDA_CATALOG + response = self.client.post( + "/rest/v2/datasets?pid_type=doi", self.cr_test_data, format="json" + ) + self.assertTrue( + response.data["research_dataset"]["preferred_identifier"].startswith("doi:10.") + ) # Test with pid_type = not_known - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/v2/datasets?pid_type=not_known', self.cr_test_data, format="json") - self.assertTrue(response.data['research_dataset']['preferred_identifier'].startswith('urn:')) + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post( + "/rest/v2/datasets?pid_type=not_known", self.cr_test_data, format="json" + ) + self.assertTrue( + response.data["research_dataset"]["preferred_identifier"].startswith("urn:") + ) # Test without pid_type - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertTrue(response.data['research_dataset']['preferred_identifier'].startswith('urn:')) + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") + self.assertTrue( + response.data["research_dataset"]["preferred_identifier"].startswith("urn:") + ) class CatalogRecordApiWriteIdentifierUniqueness(CatalogRecordApiWriteCommon): @@ -439,65 +251,27 @@ class CatalogRecordApiWriteIdentifierUniqueness(CatalogRecordApiWriteCommon): # create operations # - def test_create_catalog_record_error_preferred_identifier_cant_be_metadata_version_identifier(self): - """ - preferred_identifier can never be the same as a metadata_version_identifier in another cr, in any catalog. - """ - existing_metadata_version_identifier = CatalogRecordV2.objects.get(pk=1).metadata_version_identifier - self.cr_test_data['research_dataset']['preferred_identifier'] = existing_metadata_version_identifier - - # setting preferred_identifier is only allowed in harvested catalogs. - self.cr_test_data['data_catalog'] = 3 - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('research_dataset' in response.data.keys(), True, - 'The error should be about an error in research_dataset') - - # the error message should clearly state that the value of preferred_identifier appears in the - # field metadata_version_identifier in another record, therefore two asserts - self.assertEqual('preferred_identifier' in response.data['research_dataset'][0], True, - 'The error should be about metadata_version_identifier existing with this identifier') - self.assertEqual('metadata_version_identifier' in response.data['research_dataset'][0], True, - 'The error should be about metadata_version_identifier existing with this identifier') - - def test_create_catalog_record_error_preferred_identifier_exists_in_same_catalog(self): - """ - preferred_identifier already existing in the same data catalog is an error - """ - self.cr_test_data['research_dataset']['preferred_identifier'] = 'pid_by_harvester' - self.cr_test_data['data_catalog'] = 3 - cr_1 = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json").data - - self.cr_test_data['research_dataset']['preferred_identifier'] = \ - cr_1['research_dataset']['preferred_identifier'] - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('research_dataset' in response.data.keys(), True, - 'The error should be about an error in research_dataset') - self.assertEqual('preferred_identifier' in response.data['research_dataset'][0], True, - 'The error should be about preferred_identifier already existing') - def test_create_catalog_record_preferred_identifier_exists_in_another_catalog(self): """ preferred_identifier existing in another data catalog is not an error. """ unique_identifier = self._set_preferred_identifier_to_record(pk=1, catalog_id=1) - self.cr_test_data['research_dataset']['preferred_identifier'] = unique_identifier + self.cr_test_data["research_dataset"]["preferred_identifier"] = unique_identifier # different catalog, should be OK (not ATT catalog, so preferred_identifier being saved # can exist in other catalogs) - self.cr_test_data['data_catalog'] = 3 + self.cr_test_data["data_catalog"] = 3 - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - # - # update operations - # + # # + # # update operations + # # - def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_1(self): + def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_1( + self, + ): """ preferred_identifier existing in another data catalog is not an error. @@ -510,72 +284,12 @@ def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_1( cr.data_catalog_id = 3 cr.save() - data = self.client.get('/rest/v2/datasets/3').data - data['research_dataset']['preferred_identifier'] = unique_identifier + data = self.client.get("/rest/v2/datasets/3").data + data["research_dataset"]["preferred_identifier"] = unique_identifier - response = self.client.patch('/rest/v2/datasets/3', data, format="json") + response = self.client.patch("/rest/v2/datasets/3", data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_2(self): - """ - preferred_identifier existing in another data catalog is not an error. - - Test PATCH, when data_catalog is being updated to a different catalog - in the same request. In this case, the uniqueness check has to be executed - on the new data_catalog being passed. - - In this test, catalog is updated to 3, which should not contain a conflicting - identifier. - """ - unique_identifier = self._set_preferred_identifier_to_record(pk=1, catalog_id=1) - - data = self.client.get('/rest/v2/datasets/3').data - data['research_dataset']['preferred_identifier'] = unique_identifier - data['data_catalog'] = 3 - - response = self.client.patch('/rest/v2/datasets/3', data, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, data) - - def test_update_catalog_record_preferred_identifier_exists_in_another_catalog_3(self): - """ - preferred_identifier already existing in the same data catalog is an error, - in other catalogs than ATT: Harvester or other catalogs cant contain same - preferred_identifier twice. - - Test PATCH, when data_catalog is being updated to a different catalog - in the same request. In this case, the uniqueness check has to be executed - on the new data_catalog being passed. - - In this test, catalog is updated to 3, which should contain a conflicting - identifier, resulting in an error. - """ - - # setup the record in db which will cause conflict - unique_identifier = self._set_preferred_identifier_to_record(pk=3, catalog_id=3) - - data = {'research_dataset': self.cr_test_data['research_dataset']} - data['research_dataset']['preferred_identifier'] = unique_identifier - data['data_catalog'] = 3 - - response = self.client.patch('/rest/v2/datasets/2', data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('preferred_identifier' in response.data['research_dataset'][0], True, - 'The error should be about preferred_identifier already existing') - - def test_remote_doi_dataset_is_validated_against_datacite_format(self): - # Remote input DOI ids need to take datasets for datacite validation - cr = {'research_dataset': self.cr_test_data['research_dataset']} - cr['research_dataset']['preferred_identifier'] = 'doi:10.5061/dryad.10188854' - cr['data_catalog'] = 3 - cr['metadata_provider_org'] = 'metax' - cr['metadata_provider_user'] = 'metax' - cr['research_dataset'].pop('publisher', None) - - response = self.client.post('/rest/v2/datasets', cr, format="json") - # Publisher value is required for datacite format, so this should return Http400 - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('a required value for datacite format' in response.data['detail'][0], True, response.data) - # # helpers # @@ -585,9 +299,9 @@ def _set_preferred_identifier_to_record(self, pk=None, catalog_id=None): Set preferred_identifier to an existing record to a value, and return that value, which will then be used by the test to create or update another record. """ - unique_identifier = 'im unique yo' + unique_identifier = "im unique yo" cr = CatalogRecordV2.objects.get(pk=pk) - cr.research_dataset['preferred_identifier'] = unique_identifier + cr.research_dataset["preferred_identifier"] = unique_identifier cr.data_catalog_id = catalog_id cr.force_save() cr._handle_preferred_identifier_changed() @@ -605,52 +319,13 @@ class CatalogRecordApiWriteDatasetSchemaSelection(CatalogRecordApiWriteCommon): def _set_data_catalog_schema_to_harvester(self): dc = DataCatalog.objects.get(pk=1) - dc.catalog_json['research_dataset_schema'] = 'harvester' + dc.catalog_json["research_dataset_schema"] = "harvester" dc.save() def setUp(self): super().setUp() self._set_data_catalog_schema_to_harvester() - self.cr_test_data['research_dataset']['preferred_identifier'] = 'unique_pid' - - def test_catalog_record_with_not_found_json_schema_gets_default_schema(self): - # catalog has dataset schema, but it is not found on the server - dc = DataCatalog.objects.get(pk=1) - dc.catalog_json['research_dataset_schema'] = 'nonexisting' - dc.save() - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - # catalog has no dataset schema at all - dc = DataCatalog.objects.get(pk=1) - dc.catalog_json.pop('research_dataset_schema') - dc.save() - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - def test_catalog_record_create_with_other_schema(self): - """ - Ensure that dataset json schema validation works with other - json schemas than the default IDA - """ - self.cr_test_data['research_dataset']['remote_resources'] = [ - {'title': 'title'}, - {'title': 'title'} - ] - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - self.cr_test_data['research_dataset']['remote_resources'] = [ - {'title': 'title'}, - {'title': 'title'}, - {'woah': 'this should give a failure, since title is a required field, and it is missing'} - ] - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + self.cr_test_data["research_dataset"]["preferred_identifier"] = "unique_pid" def test_catalog_record_draft_is_validated_with_draft_schema(self): """ @@ -658,46 +333,23 @@ def test_catalog_record_draft_is_validated_with_draft_schema(self): of the chosen datacatalog. """ cr = deepcopy(self.cr_test_data) - cr['data_catalog'] = 2 # ida catalog - response = self.client.post('/rest/v2/datasets?draft', cr, format="json") + cr["data_catalog"] = 2 # ida catalog + response = self.client.post("/rest/v2/datasets?draft", cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr = response.data - cr['research_dataset']['remote_resources'] = [ - { - 'title': 'title', - 'use_category': {'identifier': 'source'} - } + cr["research_dataset"]["remote_resources"] = [ + {"title": "title", "use_category": {"identifier": "source"}} ] response = self.client.put(f'/rest/v2/datasets/{cr["id"]}', cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # ensure that dataset is validated against correct schema when publishing - response = self.client.post(f'/rpc/v2/datasets/publish_dataset?identifier={cr["id"]}', format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - - def test_catalog_record_ref_data_validation_with_other_schema(self): - """ - Ensure that dataset reference data validation and population works with other - json schemas than the default IDA. Ref data validation should be schema agnostic - """ - self.cr_test_data['research_dataset']['other_identifier'] = [ - { - 'notation': 'urn:1', - 'type': { - 'identifier': 'doi', - } - } - ] - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual( - 'uri.suomi.fi' in response.data['research_dataset']['other_identifier'][0]['type']['identifier'], - True, - 'Identifier type should have been populated with data from ref data' + response = self.client.post( + f'/rpc/v2/datasets/publish_dataset?identifier={cr["id"]}', format="json" ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) class CatalogRecordApiWriteUpdateTests(CatalogRecordApiWriteCommon): @@ -707,388 +359,71 @@ class CatalogRecordApiWriteUpdateTests(CatalogRecordApiWriteCommon): # # - def test_update_catalog_record(self): - cr = self.client.get('/rest/v2/datasets/1').data - cr['preservation_description'] = 'what' - - response = self.client.put('/rest/v2/datasets/1', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['preservation_description'], 'what') - cr = CatalogRecordV2.objects.get(pk=1) - self.assertEqual(cr.date_modified >= get_tz_aware_now_without_micros() - timedelta(seconds=5), True, - 'Timestamp should have been updated during object update') - - def test_update_catalog_record_error_using_preferred_identifier(self): - cr = self.client.get('/rest/v2/datasets/1').data - response = self.client.put('/rest/v2/datasets/%s' % cr['research_dataset']['preferred_identifier'], - { 'whatever': 123 }, format="json") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, - 'Update operation should return 404 when using preferred_identifier') - - def test_update_catalog_record_error_required_fields(self): - """ - Field 'research_dataset' is missing, which should result in an error, since PUT - replaces an object and requires all 'required' fields to be present. - """ - cr = self.client.get('/rest/v2/datasets/1').data - cr.pop('research_dataset') - response = self.client.put('/rest/v2/datasets/1', cr, format="json") - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('research_dataset' in response.data.keys(), True, - 'Error for field \'research_dataset\' is missing from response.data') - - def test_update_catalog_record_not_found(self): - response = self.client.put('/rest/v2/datasets/doesnotexist', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_update_catalog_record_contract(self): - # take any cr that has a contract set - cr = CatalogRecordV2.objects.filter(contract_id__isnull=False).first() - old_contract_id = cr.contract.id - - # update contract to any different contract - cr_1 = self.client.get('/rest/v2/datasets/%d' % cr.id).data - cr_1['contract'] = Contract.objects.all().exclude(pk=old_contract_id).first().id - - response = self.client.put('/rest/v2/datasets/%d' % cr.id, cr_1, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - new_contract_id = CatalogRecordV2.objects.get(pk=cr.id).contract.id - self.assertNotEqual(old_contract_id, new_contract_id, 'Contract should have changed') - - # - # update list operations PUT - # - - def test_catalog_record_update_list(self): - cr_1 = self.client.get('/rest/v2/datasets/1').data - cr_1['preservation_description'] = 'updated description' - - cr_2 = self.client.get('/rest/v2/datasets/2').data - cr_2['preservation_description'] = 'second updated description' - - response = self.client.put('/rest/v2/datasets', [ cr_1, cr_2 ], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['success']), 2) - - updated_cr = CatalogRecordV2.objects.get(pk=1) - self.assertEqual(updated_cr.preservation_description, 'updated description') - updated_cr = CatalogRecordV2.objects.get(pk=2) - self.assertEqual(updated_cr.preservation_description, 'second updated description') - - def test_catalog_record_update_list_error_one_fails(self): - cr_1 = self.client.get('/rest/v2/datasets/1').data - cr_1['preservation_description'] = 'updated description' - - # data catalog is a required field, should therefore fail - cr_2 = self.client.get('/rest/v2/datasets/2').data - cr_2.pop('data_catalog', None) - - response = self.client.put('/rest/v2/datasets', [ cr_1, cr_2 ], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual(isinstance(response.data['success'], list), True, - 'return data should contain key success, which is a list') - self.assertEqual(len(response.data['success']), 1) - self.assertEqual(len(response.data['failed']), 1) - - updated_cr = CatalogRecordV2.objects.get(pk=1) - self.assertEqual(updated_cr.preservation_description, 'updated description') - - def test_catalog_record_update_list_error_key_not_found(self): - # does not have identifier key - cr_1 = self.client.get('/rest/v2/datasets/1').data - cr_1.pop('id') - cr_1.pop('identifier') - cr_1['research_dataset'].pop('metadata_version_identifier') - - cr_2 = self.client.get('/rest/v2/datasets/2').data - cr_2['preservation_description'] = 'second updated description' - - response = self.client.put('/rest/v2/datasets', [ cr_1, cr_2 ], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 1) - self.assertEqual(len(response.data['failed']), 1) - - def test_catalog_record_deprecated_and_date_deprecated_cannot_be_set(self): - # Test catalog record's deprecated field cannot be set with POST, PUT or PATCH - - initial_deprecated = True - self.cr_test_data['deprecated'] = initial_deprecated - self.cr_test_data['date_deprecated'] = '2018-01-01T00:00:00' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.data['deprecated'], False) - self.assertTrue('date_deprecated' not in response.data) - - response_json = self.client.get('/rest/v2/datasets/1').data - initial_deprecated = response_json['deprecated'] - response_json['deprecated'] = not initial_deprecated - response_json['date_deprecated'] = '2018-01-01T00:00:00' - response = self.client.put('/rest/v2/datasets/1', response_json, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['deprecated'], initial_deprecated) - self.assertTrue('date_deprecated' not in response.data) - - initial_deprecated = self.client.get('/rest/v2/datasets/1').data['deprecated'] - response = self.client.patch('/rest/v2/datasets/1', { 'deprecated': not initial_deprecated }, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['deprecated'], initial_deprecated) - self.assertTrue('date_deprecated' not in response.data) - def test_change_datacatalog_ATT_to_IDA(self): cr = self._get_new_full_test_att_cr_data() # create ATT data catalog - dc_att = self._get_object_from_test_data('datacatalog', 4) - dc_att['catalog_json']['identifier'] = 'urn:nbn:fi:att:data-catalog-att' - dc_att = self.client.post('/rest/v2/datacatalogs', dc_att, format="json").data + dc_att = self._get_object_from_test_data("datacatalog", 4) + dc_att["catalog_json"]["identifier"] = "urn:nbn:fi:att:data-catalog-att" + dc_att = self.client.post("/rest/v2/datacatalogs", dc_att, format="json").data # create IDA data catalog - dc_ida = self._get_object_from_test_data('datacatalog') - dc_ida['catalog_json']['identifier'] = 'urn:nbn:fi:att:data-catalog-ida' - dc_ida = self.client.post('/rest/v2/datacatalogs', dc_ida, format="json").data + dc_ida = self._get_object_from_test_data("datacatalog") + dc_ida["catalog_json"]["identifier"] = "urn:nbn:fi:att:data-catalog-ida" + dc_ida = self.client.post("/rest/v2/datacatalogs", dc_ida, format="json").data # create ATT catalog record - cr['data_catalog'] = dc_att - cr_att = self.client.post('/rest/v2/datasets', cr, format="json").data + cr["data_catalog"] = dc_att + cr_att = self.client.post("/rest/v2/datasets", cr, format="json").data # change data catalog to IDA - cr_id = cr_att['id'] - cr_att['data_catalog']['id'] = dc_ida['id'] - cr_att['data_catalog']['identifier'] = dc_ida['catalog_json']['identifier'] - cr_ida = self.client.put('/rest/v2/datasets/%d' % cr_id, cr_att, format="json") + cr_id = cr_att["id"] + cr_att["data_catalog"]["id"] = dc_ida["id"] + cr_att["data_catalog"]["identifier"] = dc_ida["catalog_json"]["identifier"] + cr_ida = self.client.put("/rest/v2/datasets/%d" % cr_id, cr_att, format="json") self.assertEqual(cr_ida.status_code, status.HTTP_200_OK, cr_ida) - self.assertTrue(not all(item in cr_ida.data['research_dataset'].keys() for item in - ['remote_resources', 'total_remote_resources_byte_size'])) - self.assertTrue('metadata_version_identifier' in cr_ida.data['research_dataset'].keys()) + self.assertTrue( + not all( + item in cr_ida.data["research_dataset"].keys() + for item in ["remote_resources", "total_remote_resources_byte_size"] + ) + ) + self.assertTrue("metadata_version_identifier" in cr_ida.data["research_dataset"].keys()) files = { - 'files': [{ - "title": "File metadata title 1", - "file_type": { - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text", - "pref_label": { - "en": "Text", - "fi": "Teksti", - "und": "Teksti" - } - }, - "identifier": "pid:urn:1", - "use_category": { - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source", - "pref_label": { - "en": "Source material", - "fi": "Lähdeaineisto", - "und": "Lähdeaineisto" - } + "files": [ + { + "title": "File metadata title 1", + "file_type": { + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text", + "pref_label": {"en": "Text", "fi": "Teksti", "und": "Teksti"}, + }, + "identifier": "pid:urn:1", + "use_category": { + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source", + "pref_label": { + "en": "Source material", + "fi": "Lähdeaineisto", + "und": "Lähdeaineisto", + }, + }, } - }]} - cr_ida = self.client.post('/rest/v2/datasets/%d/files' % cr_id, files, format="json") - - self.assertEqual(cr_ida.status_code, status.HTTP_200_OK, cr_ida.data) - self.assertTrue(cr_ida.data['files_added'] == 1, 'Must indicate of one file addition') - - response = self.client.get('/rest/v2/datasets/%d?include_user_metadata' % cr_id).data - self.assertTrue(len(response['research_dataset']['files']) == 1, 'Dataset must contain one file') - - -class CatalogRecordApiWritePartialUpdateTests(CatalogRecordApiWriteCommon): - # - # - # update apis PATCH - # - # - - def test_update_catalog_record_partial(self): - new_data_catalog = self._get_object_from_test_data('datacatalog', requested_index=1)['id'] - new_data = { - "data_catalog": new_data_catalog, + ] } - response = self.client.patch('/rest/v2/datasets/%s' % self.identifier, new_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('research_dataset' in response.data.keys(), True, 'PATCH operation should return full content') - self.assertEqual(response.data['data_catalog']['id'], new_data_catalog, 'Field data_catalog was not updated') - - # - # update list operations PATCH - # - - def test_catalog_record_partial_update_list(self): - test_data = {} - test_data['id'] = 1 - test_data['preservation_description'] = 'description' - - second_test_data = {} - second_test_data['id'] = 2 - second_test_data['preservation_description'] = 'description 2' - - response = self.client.patch('/rest/v2/datasets', [test_data, second_test_data], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data, True, 'response.data should contain list of changed objects') - self.assertEqual(len(response.data), 2, 'response.data should contain 2 changed objects') - self.assertEqual('research_dataset' in response.data['success'][0]['object'], True, - 'response.data should contain full objects') - - updated_cr = CatalogRecordV2.objects.get(pk=1) - self.assertEqual(updated_cr.preservation_description, 'description') - - def test_catalog_record_partial_update_list_error_one_fails(self): - test_data = {} - test_data['id'] = 1 - test_data['preservation_description'] = 'description' - - second_test_data = {} - second_test_data['preservation_state'] = 555 # value not allowed - second_test_data['id'] = 2 - - response = self.client.patch('/rest/v2/datasets', [test_data, second_test_data], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 1, 'success list should contain one item') - self.assertEqual(len(response.data['failed']), 1, 'there should have been one failed element') - self.assertEqual('preservation_state' in response.data['failed'][0]['errors'], True, - response.data['failed'][0]['errors']) - - def test_catalog_record_partial_update_list_error_key_not_found(self): - # does not have identifier key - test_data = {} - test_data['preservation_state'] = 10 - - second_test_data = {} - second_test_data['id'] = 2 - second_test_data['preservation_state'] = 20 - - response = self.client.patch('/rest/v2/datasets', [test_data, second_test_data], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 1, 'success list should contain one item') - self.assertEqual(len(response.data['failed']), 1, 'there should have been one failed element') - self.assertEqual('detail' in response.data['failed'][0]['errors'], True, response.data['failed'][0]['errors']) - self.assertEqual('identifying key' in response.data['failed'][0]['errors']['detail'][0], True, - response.data['failed'][0]['errors']) + cr_ida = self.client.post("/rest/v2/datasets/%d/files" % cr_id, files, format="json") + self.assertEqual(cr_ida.status_code, status.HTTP_200_OK, cr_ida.data) + self.assertTrue(cr_ida.data["files_added"] == 1, "Must indicate of one file addition") -class CatalogRecordApiWriteDeleteTests(CatalogRecordApiWriteCommon): - # - # - # - # delete apis - # - # - # + response = self.client.get("/rest/v2/datasets/%d?include_user_metadata" % cr_id).data + self.assertTrue( + len(response["research_dataset"]["files"]) == 1, + "Dataset must contain one file", + ) - def test_delete_catalog_record(self): - url = '/rest/v2/datasets/%s' % self.identifier - response = self.client.delete(url) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - response = self.client.get(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - try: - deleted_catalog_record = CatalogRecordV2.objects.get(identifier=self.identifier) - raise Exception('Deleted CatalogRecord should not be retrievable from the default objects table') - except CatalogRecordV2.DoesNotExist: - # successful test should go here, instead of raising the expection in try: block - pass - - try: - deleted_catalog_record = CatalogRecordV2.objects_unfiltered.get(identifier=self.identifier) - except CatalogRecordV2.DoesNotExist: - raise Exception('Deleted CatalogRecord should not be deleted from the db, but marked as removed') - - self.assertEqual(deleted_catalog_record.removed, True) - self.assertEqual(deleted_catalog_record.identifier, self.identifier) - self.assertEqual(deleted_catalog_record.date_modified, deleted_catalog_record.date_removed, - 'date_modified should be updated') - - def test_delete_catalog_record_error_using_preferred_identifier(self): - url = '/rest/v2/datasets/%s' % self.preferred_identifier - response = self.client.delete(url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_bulk_delete_catalog_record_permissions(self): - # create catalog with 'metax' edit permissions and create dataset with this catalog as 'metax' user - cr = self._get_new_test_cr_data() - cr.pop('id') - catalog = self._get_object_from_test_data('datacatalog', requested_index=0) - catalog.pop('id') - catalog['catalog_json']['identifier'] = 'metax-catalog' - catalog['catalog_record_services_edit'] = 'metax' - catalog = self.client.post('/rest/v2/datacatalogs', catalog, format="json") - cr['data_catalog'] = {'id': catalog.data['id'], 'identifier': catalog.data['catalog_json']['identifier']} - - self._use_http_authorization(username='metax') - response = self.client.post('/rest/v2/datasets/', cr, format="json") - metax_cr = response.data['id'] - - # create catalog with 'testuser' edit permissions and create dataset with this catalog as 'testuser' user - cr = self._get_new_test_cr_data() - cr.pop('id') - catalog = self._get_object_from_test_data('datacatalog', requested_index=1) - catalog.pop('id') - catalog['catalog_json']['identifier'] = 'testuser-catalog' - catalog['catalog_record_services_edit'] = 'testuser' - catalog = self.client.post('/rest/v2/datacatalogs', catalog, format="json") - cr['data_catalog'] = {'id': catalog.data['id'], 'identifier': catalog.data['catalog_json']['identifier']} - - self._use_http_authorization(username='testuser', password='testuserpassword') - response = self.client.post('/rest/v2/datasets/', cr, format="json") - testuser_cr = response.data['id'] - - # after trying to delete as 'testuser' only one catalog is deleted - response = self.client.delete('/rest/v2/datasets', [metax_cr, testuser_cr], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.post('/rest/v2/datasets/list?pagination=false', [metax_cr, testuser_cr], format="json") - self.assertTrue(len(response.data), 1) - - # deleting from list of not accessable resource - response = self.client.delete('/rest/datasets', [metax_cr], format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - response = self.client.post('/rest/v2/datasets/list?pagination=false', [metax_cr, testuser_cr], format="json") - self.assertTrue(len(response.data), 1) - - def test_bulk_delete_catalog_record(self): - ids = [1, 2, 3] - identifiers = CatalogRecordV2.objects.filter(pk__in=[4, 5, 6]).values_list('identifier', flat=True) - - for crs in [ids, identifiers]: - response = self.client.delete('/rest/datasets', crs, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data == [1, 2, 3] or response.data == [4, 5, 6]) - response = self.client.post('/rest/datasets/list?pagination=false', crs, format="json") - self.assertFalse(response.data) - - for cr in crs: - if isinstance(cr, int): - deleted = CatalogRecordV2.objects_unfiltered.get(id=cr) - else: - deleted = CatalogRecordV2.objects_unfiltered.get(identifier=cr) - - self.assertEqual(deleted.removed, True) - self.assertEqual(deleted.date_modified, deleted.date_removed, - 'date_modified should be updated') - - # failing tests - ids = [1000, 2000] - identifiers = ['1000', '2000'] - - for crs in [ids, identifiers]: - response = self.client.delete('/rest/datasets', ids, format="json") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - ids = [] - response = self.client.delete('/rest/datasets', ids, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue('Received empty list of identifiers' in response.data['detail'][0]) class CatalogRecordApiWriteAlternateRecords(CatalogRecordApiWriteCommon): @@ -1105,215 +440,8 @@ class CatalogRecordApiWriteAlternateRecords(CatalogRecordApiWriteCommon): def setUp(self): super(CatalogRecordApiWriteAlternateRecords, self).setUp() self.preferred_identifier = self._set_preferred_identifier_to_record(pk=1, data_catalog=1) - self.cr_test_data['research_dataset']['preferred_identifier'] = self.preferred_identifier - self.cr_test_data['data_catalog'] = None - - def test_alternate_record_set_is_created_if_it_doesnt_exist(self): - """ - Add a record, where a record already existed with the same pref_id, but did not have an - alternate_record_set yet. Ensure a new set is created, and both records are added to it. - """ - - # new record is saved to catalog 3, which does not support versioning - self.cr_test_data['data_catalog'] = 3 - - existing_records_count = CatalogRecordV2.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}).count() - self.assertEqual(existing_records_count, 1, - 'in the beginning, there should be only one record with pref id %s' - % self.preferred_identifier) - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - records = CatalogRecordV2.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}) - self.assertEqual(len(records), 2, - 'after, there should be two records with pref id %s' % self.preferred_identifier) - - # both records are moved to same set - ars_id = records[0].alternate_record_set.id - self.assertEqual(records[0].alternate_record_set.id, ars_id) - self.assertEqual(records[1].alternate_record_set.id, ars_id) - - # records in the set are the ones expected - self.assertEqual(records[0].id, 1) - self.assertEqual(records[1].id, response.data['id']) - - # records in the set are indeed in different catalogs - self.assertEqual(records[0].data_catalog.id, 1) - self.assertEqual(records[1].data_catalog.id, 3) - - def test_append_to_existing_alternate_record_set_if_it_exists(self): - """ - An alternate_record_set already exists with two records in it. Create a third - record with the same preferred_identifier. The created record should be added - to the existing alternate_record_set. - """ - self._set_preferred_identifier_to_record(pk=2, data_catalog=2) - self.cr_test_data['data_catalog'] = 3 - - existing_records_count = CatalogRecordV2.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}).count() - self.assertEqual(existing_records_count, 2, - 'in the beginning, there should be two records with pref id %s' % self.preferred_identifier) - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - records = CatalogRecordV2.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}) - self.assertEqual(len(records), 3, - 'after, there should be three records with pref id %s' % self.preferred_identifier) - - # all records belong to same set - ars_id = records[0].alternate_record_set.id - self.assertEqual(records[0].alternate_record_set.id, ars_id) - self.assertEqual(records[1].alternate_record_set.id, ars_id) - self.assertEqual(records[2].alternate_record_set.id, ars_id) - - # records in the set are the ones expected - self.assertEqual(records[0].id, 1) - self.assertEqual(records[1].id, 2) - self.assertEqual(records[2].id, response.data['id']) - - # records in the set are indeed in different catalogs - self.assertEqual(records[0].data_catalog.id, 1) - self.assertEqual(records[1].data_catalog.id, 2) - self.assertEqual(records[2].data_catalog.id, 3) - - def test_record_is_removed_from_alternate_record_set_when_deleted(self): - """ - When a record belong to an alternate_record_set with multiple other records, - only the records itself should be deleted. The alternate_record_set should keep - existing for the other records. - """ - - # initial conditions will have 3 records in the same set. - self._set_and_ensure_initial_conditions() - - response = self.client.delete('/rest/v2/datasets/2', format="json") - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - # check resulting conditions - records = CatalogRecordV2.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}) - self.assertEqual(records[0].alternate_record_set.records.count(), 2) - - def test_alternate_record_set_is_deleted_if_updating_record_with_no_versioning_and_one_record_left(self): - """ - Same as above, but updating a record in a catalog, which does NOT support versioning. - In this case, the the records itself gets updated, and removed from the old alternate_record_set. - Since the old alternate_record_set is left with only one other record, the alternate set - should be deleted. - """ - original_preferred_identifier = self.preferred_identifier - - # after this, pk=1 and pk=2 have the same preferred_identifier, in catalogs 1 and 3. - # note! catalog=3, so an update will not create a new version! - self._set_preferred_identifier_to_record(pk=2, data_catalog=3) - - # save for later checking - old_ars_id = CatalogRecordV2.objects.get(pk=2).alternate_record_set.id - - # retrieve record id=2, and change its preferred identifier - response = self.client.get('/rest/v2/datasets/2', format="json") - data = {'research_dataset': response.data['research_dataset']} - data['research_dataset']['preferred_identifier'] = 'a:new:identifier:here' - - # updating preferred_identifier - a new version is NOT created - response = self.client.patch('/rest/v2/datasets/2', data=data, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - records = CatalogRecordV2.objects.filter( - research_dataset__contains={'preferred_identifier': original_preferred_identifier }) - self.assertEqual(records.count(), 1) - - with self.assertRaises(AlternateRecordSet.DoesNotExist, msg='alternate record set should have been deleted'): - AlternateRecordSet.objects.get(pk=old_ars_id) - - def test_alternate_record_set_is_deleted_if_deleting_record_and_only_one_record_left(self): - """ - Same princible as above, but through deleting a record, instead of updating a record. - - End result for the alternate_record_set should be the same (it gets deleted). - """ - self._set_preferred_identifier_to_record(pk=2, data_catalog=2) - old_ars_id = CatalogRecordV2.objects.get(pk=2).alternate_record_set.id - - response = self.client.delete('/rest/v2/datasets/2', format="json") - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - records = CatalogRecordV2.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}) - self.assertEqual(records.count(), 1, 'should be only record with this identifier left now') - - with self.assertRaises(AlternateRecordSet.DoesNotExist, msg='alternate record set should have been deleted'): - AlternateRecordSet.objects.get(pk=old_ars_id) - - def test_alternate_record_set_is_included_in_responses(self): - """ - Details of a dataset should contain field alternate_record_set in it. - For a particular record, the set should not contain its own metadata_version_identifier in the set. - """ - self.cr_test_data['data_catalog'] = 3 - msg_self_should_not_be_listed = 'identifier of the record itself should not be listed' - - response_1 = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - response_2 = self.client.get('/rest/v2/datasets/1', format="json") - self.assertEqual(response_1.status_code, status.HTTP_201_CREATED) - self.assertEqual('alternate_record_set' in response_1.data, True) - self.assertEqual( - response_1.data['identifier'] - not in response_1.data['alternate_record_set'], - True, - msg_self_should_not_be_listed - ) - self.assertEqual( - response_2.data['identifier'] - in response_1.data['alternate_record_set'], - True - ) - - self.cr_test_data.update({'data_catalog': 4}) - response_3 = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response_3.status_code, status.HTTP_201_CREATED) - self.assertEqual('alternate_record_set' in response_3.data, True) - self.assertEqual( - response_1.data['identifier'] - in response_3.data['alternate_record_set'], - True - ) - self.assertEqual( - response_2.data['identifier'] - in response_3.data['alternate_record_set'], - True - ) - self.assertEqual( - response_3.data['identifier'] - not in response_3.data['alternate_record_set'], - True, - msg_self_should_not_be_listed - ) - - response_2 = self.client.get('/rest/v2/datasets/1', format="json") - self.assertEqual('alternate_record_set' in response_2.data, True) - self.assertEqual( - response_1.data['identifier'] - in response_2.data['alternate_record_set'], - True - ) - self.assertEqual( - response_3.data['identifier'] - in response_2.data['alternate_record_set'], - True - ) - self.assertEqual( - response_2.data['identifier'] - not in response_2.data['alternate_record_set'], - True, - msg_self_should_not_be_listed - ) + self.cr_test_data["research_dataset"]["preferred_identifier"] = self.preferred_identifier + self.cr_test_data["data_catalog"] = None def _set_preferred_identifier_to_record(self, pk=1, data_catalog=1): """ @@ -1323,147 +451,82 @@ def _set_preferred_identifier_to_record(self, pk=1, data_catalog=1): Note that if calling this method several times, this will also create an alternate_record_set (by calling _handle_preferred_identifier_changed()). """ - unique_identifier = 'im unique yo' + unique_identifier = "im unique yo" cr = CatalogRecordV2.objects.get(pk=pk) - cr.research_dataset['preferred_identifier'] = unique_identifier + cr.research_dataset["preferred_identifier"] = unique_identifier cr.data_catalog_id = data_catalog cr.force_save() cr._handle_preferred_identifier_changed() return unique_identifier - def _set_and_ensure_initial_conditions(self): - """ - Update two existing records to have same pref_id and be in different catalogs, - to create an alternate_record_set. - """ - - # pk=1 also shares the same preferred_identifier (has been set in setUp()) - self._set_preferred_identifier_to_record(pk=2, data_catalog=3) - self._set_preferred_identifier_to_record(pk=3, data_catalog=4) - - # ensuring initial conditions... - records = CatalogRecordV2.objects.filter( - research_dataset__contains={'preferred_identifier': self.preferred_identifier}) - self.assertEqual(len(records), 3, - 'in the beginning, there should be three records with pref id %s' % self.preferred_identifier) - ars_id = records[0].alternate_record_set.id - self.assertEqual(records[0].alternate_record_set.id, ars_id) - self.assertEqual(records[1].alternate_record_set.id, ars_id) - self.assertEqual(records[2].alternate_record_set.id, ars_id) - - def test_update_rd_title_creates_new_metadata_version(self): - """ - Updating the title of metadata should create a new metadata version. - """ - response_1 = self._get_and_update_title(self.pk) - self.assertEqual(response_1.status_code, status.HTTP_200_OK, response_1.data) - self._assert_metadata_version_count(response_1.data, 2) - - # get list of metadata versions to access contents... - response = self.client.get('/rest/v2/datasets/%d/metadata_versions' % response_1.data['id'], format="json") - - response_2 = self.client.get('/rest/v2/datasets/%d/metadata_versions/%s' % - (self.pk, response.data[0]['metadata_version_identifier']), format="json") - self.assertEqual(response_2.status_code, status.HTTP_200_OK, response_2.data) - self.assertEqual('preferred_identifier' in response_2.data, True) - - # note! response_1 == cr, response_2 == rd - self.assertEqual(response_1.data['research_dataset']['preferred_identifier'], - response_2.data['preferred_identifier']) - def test_dataset_version_lists_removed_records(self): # create version2 of a record - response = self.client.post('/rpc/v2/datasets/create_new_version?identifier=1', format="json") + response = self.client.post( + "/rpc/v2/datasets/create_new_version?identifier=1", format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - new_version_id = response.data['id'] + new_version_id = response.data["id"] # publish the new version - response = self.client.post(f'/rpc/v2/datasets/publish_dataset?identifier={new_version_id}', format="json") + response = self.client.post( + f"/rpc/v2/datasets/publish_dataset?identifier={new_version_id}", + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # delete version2 - response = self.client.delete(f'/rest/v2/datasets/{new_version_id}', format="json") + response = self.client.delete(f"/rest/v2/datasets/{new_version_id}", format="json") self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) # check date_removed is listed and not None in deleted version - response = self.client.get('/rest/v2/datasets/1', format="json") + response = self.client.get("/rest/v2/datasets/1", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data['dataset_version_set'][0].get('date_removed')) - self.assertTrue(response.data['dataset_version_set'][0].get('date_removed') is not None) - self.assertFalse(response.data['dataset_version_set'][1].get('date_removed')) - - def _assert_metadata_version_count(self, record, count): - response = self.client.get('/rest/v2/datasets/%d/metadata_versions' % record['id'], format="json") - self.assertEqual(len(response.data), count) - - def _set_cr_to_catalog(self, pk=None, dc=None): - cr = CatalogRecordV2.objects.get(pk=pk) - cr.data_catalog_id = dc - cr.force_save() - - def _get_and_update_title(self, pk, params=None): - """ - Get, modify, and update data for given pk. The modification should cause a new - version to be created if the catalog permits. - - Should not force preferred_identifier to change. - """ - data = self.client.get('/rest/v2/datasets/%d' % pk, format="json").data - data['research_dataset']['title']['en'] = 'modified title' - return self.client.put('/rest/v2/datasets/%d%s' % (pk, params or ''), data, format="json") + self.assertTrue(response.data["dataset_version_set"][0].get("date_removed")) + self.assertTrue(response.data["dataset_version_set"][0].get("date_removed") is not None) + self.assertFalse(response.data["dataset_version_set"][1].get("date_removed")) def test_allow_metadata_changes_after_deprecation(self): """ For deprecated datasets metadata changes are still allowed. Changing user metadata for files that are marked as removed (caused the deprecation) is not possible. """ - response = self.client.get('/rest/v2/datasets/1?include_user_metadata') + response = self.client.get("/rest/v2/datasets/1?include_user_metadata") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr = response.data - response = self.client.delete('/rest/v2/files/1') + response = self.client.delete("/rest/v2/files/1") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # after the dataset is deprecated, metadata updates should still be ok - cr['research_dataset']['description'] = { + cr["research_dataset"]["description"] = { "en": "Updating new description for deprecated dataset should not create any problems" } - response = self.client.put('/rest/v2/datasets/%s' % cr['id'], cr, format="json") + response = self.client.put("/rest/v2/datasets/%s" % cr["id"], cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue('new description' in response.data['research_dataset']['description']['en'], - 'description field should be updated') + self.assertTrue( + "new description" in response.data["research_dataset"]["description"]["en"], + "description field should be updated", + ) - file_changes = { - 'files': [ cr['research_dataset']['files'][0] ] - } + file_changes = {"files": [cr["research_dataset"]["files"][0]]} - file_changes['files'][0]['title'] = 'Brand new title 1' + file_changes["files"][0]["title"] = "Brand new title 1" - response = self.client.put('/rest/v2/datasets/%s/files/user_metadata' % cr['id'], file_changes, format="json") + response = self.client.put( + "/rest/v2/datasets/%s/files/user_metadata" % cr["id"], + file_changes, + format="json", + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('The following files are not' in response.data['detail'][0], True, response.data) - - -class CatalogRecordApiWriteRemoteResources(CatalogRecordApiWriteCommon): - - """ - remote_resources related tests - """ - - def test_calculate_total_remote_resources_byte_size(self): - cr_with_rr = self._get_object_from_test_data('catalogrecord', requested_index=14) - rr = cr_with_rr['research_dataset']['remote_resources'] - total_remote_resources_byte_size = sum(res['byte_size'] for res in rr) - self.cr_att_test_data['research_dataset']['remote_resources'] = rr - response = self.client.post('/rest/v2/datasets', self.cr_att_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('total_remote_resources_byte_size' in response.data['research_dataset'], True) - self.assertEqual(response.data['research_dataset']['total_remote_resources_byte_size'], - total_remote_resources_byte_size) + self.assertEqual( + "The following files are not" in response.data["detail"][0], + True, + response.data, + ) class CatalogRecordApiWriteLegacyDataCatalogs(CatalogRecordApiWriteCommon): @@ -1477,76 +540,24 @@ def setUp(self): Create a test-datacatalog that plays the role of a legacy catalog. """ super().setUp() - dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema='att').first() - dc.catalog_json['identifier'] = LEGACY_CATALOGS[0] + dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema="att").first() + dc.catalog_json["identifier"] = LEGACY_CATALOGS[0] dc.force_save() - del self.cr_test_data['research_dataset']['files'] - del self.cr_test_data['research_dataset']['total_files_byte_size'] - - def test_legacy_catalog_pids_are_not_unique(self): - # values provided as pid values in legacy catalogs are not required to be unique - # within the catalog. - self.cr_test_data['data_catalog'] = LEGACY_CATALOGS[0] - self.cr_test_data['research_dataset']['preferred_identifier'] = 'a' - same_pid_ids = [] - for i in range(3): - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], 'a') - same_pid_ids.append(response.data['id']) - - # pid can even be same as an existing dataset's pid in an ATT catalog - real_pid = CatalogRecordV2.objects.get(pk=1).preferred_identifier - self.cr_test_data['research_dataset']['preferred_identifier'] = real_pid - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], real_pid) + del self.cr_test_data["research_dataset"]["files"] + del self.cr_test_data["research_dataset"]["total_files_byte_size"] def test_legacy_catalog_pid_must_be_provided(self): # pid cant be empty string - self.cr_test_data['data_catalog'] = LEGACY_CATALOGS[0] - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + self.cr_test_data["data_catalog"] = LEGACY_CATALOGS[0] + self.cr_test_data["research_dataset"]["preferred_identifier"] = "" + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) # pid cant be omitted - del self.cr_test_data['research_dataset']['preferred_identifier'] - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + del self.cr_test_data["research_dataset"]["preferred_identifier"] + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - def test_legacy_catalog_pids_update(self): - # test setup - self.cr_test_data['data_catalog'] = LEGACY_CATALOGS[0] - self.cr_test_data['research_dataset']['preferred_identifier'] = 'a' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - # update record. in updates uniqueness also should not be checked - modify = response.data - real_pid = CatalogRecordV2.objects.get(pk=1).preferred_identifier - modify['research_dataset']['preferred_identifier'] = real_pid - response = self.client.put('/rest/v2/datasets/%s?include_legacy' % modify['id'], modify, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_delete_legacy_catalog_dataset(self): - """ - Datasets in legacy catalogs should be deleted permanently, instead of only marking them - as 'removed'. - """ - - # test setup - self.cr_test_data['data_catalog'] = LEGACY_CATALOGS[0] - self.cr_test_data['research_dataset']['preferred_identifier'] = 'a' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] - - # delete record - response = self.client.delete('/rest/v2/datasets/%s?include_legacy' % cr_id, format="json") - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - results_count = CatalogRecordV2.objects_unfiltered.filter(pk=cr_id).count() - self.assertEqual(results_count, 0, 'record should have been deleted permantly') - class CatalogRecordApiWriteOwnerFields(CatalogRecordApiWriteCommon): @@ -1564,325 +575,49 @@ def test_metadata_owner_org_is_copied_from_metadata_provider_org(self): """ # create - cr = self.client.get('/rest/v2/datasets/1', format="json").data - cr.pop('id') - cr.pop('identifier') - cr.pop('metadata_owner_org') - cr['research_dataset'].pop('preferred_identifier') - response = self.client.post('/rest/v2/datasets', cr, format="json") + cr = self.client.get("/rest/v2/datasets/1", format="json").data + cr.pop("id") + cr.pop("identifier") + cr.pop("metadata_owner_org") + cr["research_dataset"].pop("preferred_identifier") + response = self.client.post("/rest/v2/datasets", cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['metadata_owner_org'], response.data['metadata_provider_org']) + self.assertEqual( + response.data["metadata_owner_org"], response.data["metadata_provider_org"] + ) # update to null - update is prevented - cr = self.client.get('/rest/v2/datasets/1', format="json").data - original = cr['metadata_owner_org'] - cr['metadata_owner_org'] = None - response = self.client.put('/rest/v2/datasets/1', cr, format="json") + cr = self.client.get("/rest/v2/datasets/1", format="json").data + original = cr["metadata_owner_org"] + cr["metadata_owner_org"] = None + response = self.client.put("/rest/v2/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['metadata_owner_org'], original) + self.assertEqual(response.data["metadata_owner_org"], original) # update with patch, where metadata_owner_org field is absent - value is not reverted back # to metadata_provider_org - response = self.client.patch('/rest/v2/datasets/1', { 'metadata_owner_org': 'abc' }, format="json") + response = self.client.patch( + "/rest/v2/datasets/1", {"metadata_owner_org": "abc"}, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.patch('/rest/v2/datasets/1', { 'contract': 1 }, format="json") - self.assertEqual(response.data['metadata_owner_org'], 'abc') + response = self.client.patch("/rest/v2/datasets/1", {"contract": 1}, format="json") + self.assertEqual(response.data["metadata_owner_org"], "abc") def test_metadata_provider_org_is_readonly_after_creating(self): - cr = self.client.get('/rest/v2/datasets/1', format="json").data - original = cr['metadata_provider_org'] - cr['metadata_provider_org'] = 'changed' - response = self.client.put('/rest/v2/datasets/1', cr, format="json") + cr = self.client.get("/rest/v2/datasets/1", format="json").data + original = cr["metadata_provider_org"] + cr["metadata_provider_org"] = "changed" + response = self.client.put("/rest/v2/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['metadata_provider_org'], original) + self.assertEqual(response.data["metadata_provider_org"], original) def test_metadata_provider_user_is_readonly_after_creating(self): - cr = self.client.get('/rest/v2/datasets/1', format="json").data - original = cr['metadata_provider_user'] - cr['metadata_provider_user'] = 'changed' - response = self.client.put('/rest/v2/datasets/1', cr, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['metadata_provider_user'], original) - - -class CatalogRecordApiEndUserAccess(CatalogRecordApiWriteCommon): - - """ - End User Access -related permission testing. - """ - - def setUp(self): - super().setUp() - - # create catalogs with end user access permitted - dc = DataCatalog.objects.get(pk=1) - catalog_json = dc.catalog_json - for identifier in END_USER_ALLOWED_DATA_CATALOGS: - catalog_json['identifier'] = identifier - dc = DataCatalog.objects.create( - catalog_json=catalog_json, - date_created=get_tz_aware_now_without_micros(), - catalog_record_services_create='testuser,api_auth_user,metax', - catalog_record_services_edit='testuser,api_auth_user,metax' - ) - - self.token = get_test_oidc_token() - - # by default, use the unmodified token. to use a different/modified token - # for various test scenarions, alter self.token, and call the below method again - self._use_http_authorization(method='bearer', token=self.token) - - # no reason to test anything related to failed authentication, since failed - # authentication stops the request from proceeding anywhere - self._mock_token_validation_succeeds() - - def _set_cr_owner_to_token_user(self, cr_id): - cr = CatalogRecordV2.objects.get(pk=cr_id) - cr.user_created = self.token['CSCUserName'] - cr.metadata_provider_user = self.token['CSCUserName'] - cr.editor = None # pretend the record was created by user directly - cr.force_save() - - def _set_cr_to_permitted_catalog(self, cr_id): - cr = CatalogRecordV2.objects.get(pk=cr_id) - cr.data_catalog_id = DataCatalog.objects.get(catalog_json__identifier=END_USER_ALLOWED_DATA_CATALOGS[0]).id - cr.force_save() - - @responses.activate - def test_user_can_create_dataset(self): - ''' - Ensure end user can create a new dataset, and required fields are - automatically placed and the user is only able to affect allowed - fields - ''' - user_created = self.token['CSCUserName'] - metadata_provider_user = self.token['CSCUserName'] - metadata_provider_org = self.token['schacHomeOrganization'] - metadata_owner_org = self.token['schacHomeOrganization'] - - self.cr_test_data['data_catalog'] = END_USER_ALLOWED_DATA_CATALOGS[0] # ida - self.cr_test_data['contract'] = 1 - self.cr_test_data['editor'] = { 'nope': 'discarded by metax' } - self.cr_test_data['preservation_description'] = 'discarded by metax' - self.cr_test_data['preservation_reason_description'] = 'discarded by metax' - self.cr_test_data['preservation_state'] = 10 - self.cr_test_data.pop('metadata_provider_user', None) - self.cr_test_data.pop('metadata_provider_org', None) - self.cr_test_data.pop('metadata_owner_org', None) - - # test file permission checking in another test - self.cr_test_data['research_dataset'].pop('files', None) - self.cr_test_data['research_dataset'].pop('directories', None) - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.content) - self.assertEqual(response.data['user_created'], user_created) - self.assertEqual(response.data['metadata_provider_user'], metadata_provider_user) - self.assertEqual(response.data['metadata_provider_org'], metadata_provider_org) - self.assertEqual(response.data['metadata_owner_org'], metadata_owner_org) - self.assertEqual('contract' in response.data, False) - self.assertEqual('editor' in response.data, False) - self.assertEqual('preservation_description' in response.data, False) - self.assertEqual('preservation_reason_description' in response.data, False) - self.assertEqual(response.data['preservation_state'], 0) - - @responses.activate - def test_user_can_create_datasets_only_to_limited_catalogs(self): - ''' - End users should not be able to create datasets for example to harvested - data catalogs. - ''' - - # test file permission checking in another test - self.cr_test_data['research_dataset'].pop('files', None) - self.cr_test_data['research_dataset'].pop('directories', None) - - # should not work - self.cr_test_data['data_catalog'] = 1 - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - # check error has expected error description - self.assertEqual('selected data catalog' in response.data['detail'][0], True, response.data) - - # should work - for identifier in END_USER_ALLOWED_DATA_CATALOGS: - if identifier in LEGACY_CATALOGS: - self.cr_test_data['research_dataset']['preferred_identifier'] = 'a' - - params = 'draft' if identifier == DFT_CATALOG else '' - self.cr_test_data['data_catalog'] = identifier - response = self.client.post(f'/rest/v2/datasets?{params}', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - if identifier in LEGACY_CATALOGS: - # prevents next test from crashing if legacy catalog is not the last in the list - del self.cr_test_data['research_dataset']['preferred_identifier'] - - @responses.activate - def test_owner_can_edit_dataset(self): - ''' - Ensure end users are able to edit datasets owned by them. - Ensure end users can only edit permitted fields. - Note: File project permissions should not be checked, since files are not changed. - ''' - - # create test record - self.cr_test_data['data_catalog'] = END_USER_ALLOWED_DATA_CATALOGS[0] - self.cr_test_data['research_dataset'].pop('files', None) # test file permission checking in another test - self.cr_test_data['research_dataset'].pop('directories', None) - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - modified_data = response.data - # research_dataset is the only permitted field to edit - modified_data['research_dataset']['value'] = 112233 - modified_data['contract'] = 1 - modified_data['editor'] = { 'nope': 'discarded by metax' } - modified_data['preservation_description'] = 'discarded by metax' - modified_data['preservation_reason_description'] = 'discarded by metax' - modified_data['preservation_state'] = 10 - - response = self.client.put('/rest/v2/datasets/%d' % modified_data['id'], modified_data, format="json") + cr = self.client.get("/rest/v2/datasets/1", format="json").data + original = cr["metadata_provider_user"] + cr["metadata_provider_user"] = "changed" + response = self.client.put("/rest/v2/datasets/1", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['research_dataset']['value'], 112233) # value we set - self.assertEqual(response.data['user_modified'], self.token['CSCUserName']) # set by metax - - # none of these should have been affected - self.assertEqual('contract' in response.data, False) - self.assertEqual('editor' in response.data, False) - self.assertEqual('preservation_description' in response.data, False) - self.assertEqual('preservation_reason_description' in response.data, False) - self.assertEqual(response.data['preservation_state'], 0) - - @responses.activate - def test_owner_can_edit_datasets_only_in_permitted_catalogs(self): - ''' - Ensure end users are able to edit datasets only in permitted catalogs, even if they - own the record (catalog may be disabled from end user editing for reason or another). - ''' - - # create test record - self.cr_test_data['data_catalog'] = 1 - self.cr_test_data['user_created'] = self.token['CSCUserName'] - self.cr_test_data['metadata_provider_user'] = self.token['CSCUserName'] - self.cr_test_data.pop('editor', None) - - self._use_http_authorization() # create cr as a service-user - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - modified_data = response.data - modified_data['research_dataset']['value'] = 112233 - - self._use_http_authorization(method='bearer', token=self.token) - response = self.client.put('/rest/v2/datasets/%d' % modified_data['id'], modified_data, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - @responses.activate - def test_other_users_cant_edit_dataset(self): - ''' - Ensure end users are unable edit datasets not owned by them. - ''' - response = self.client.get('/rest/v2/datasets/1', format="json") - modified_data = response.data - modified_data['research_dataset']['value'] = 112233 - - response = self.client.put('/rest/v2/datasets/1', modified_data, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - response = self.client.put('/rest/v2/datasets', [modified_data], format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - # ^ individual errors do not have error codes, only the general request - # has an error code for a failed request. - - @responses.activate - def test_user_can_delete_dataset(self): - self._set_cr_owner_to_token_user(1) - self._set_cr_to_permitted_catalog(1) - response = self.client.delete('/rest/v2/datasets/1', format="json") - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - - @responses.activate - def test_user_file_permissions_are_checked_during_dataset_create(self): - ''' - Ensure user's association with a project is checked during dataset create when - attaching files or directories to a dataset. - ''' - - # try creating without proper permisisons - self.cr_test_data['data_catalog'] = END_USER_ALLOWED_DATA_CATALOGS[0] # ida - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.content) - - # add project membership to user's token and try again - file_identifier = self.cr_test_data['research_dataset']['files'][0]['identifier'] - project_identifier = File.objects.get(identifier=file_identifier).project_identifier - self.token['group_names'].append('IDA01:%s' % project_identifier) - self._use_http_authorization(method='bearer', token=self.token) - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.content) - - @responses.activate - def test_user_file_permissions_are_checked_during_dataset_update(self): - ''' - Ensure user's association with a project is checked during dataset update when - attaching files or directories to a dataset. The permissions should be checked - only for changed files (newly added, or removed). - ''' - # get some files to add to another dataset - new_files = CatalogRecordV2.objects.get(pk=1).research_dataset['files'] - - self.cr_test_data['data_catalog'] = END_USER_ALLOWED_DATA_CATALOGS[0] # ida - self.cr_test_data['research_dataset'].pop('files', None) - self.cr_test_data['research_dataset'].pop('directories', None) - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - cr_id = response.data['id'] - - file_changes = { - 'files': new_files - } - - # should fail, since user's token has no permission for the newly added files - response = self.client.post(f'/rest/v2/datasets/{cr_id}/files', file_changes, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.content) - - # add project membership to user's token and try again - project_identifier = File.objects.get(identifier=new_files[0]['identifier']).project_identifier - self.token['group_names'].append('IDA01:%s' % project_identifier) - self._use_http_authorization(method='bearer', token=self.token) - - response = self.client.post(f'/rest/v2/datasets/{cr_id}/files', file_changes, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.content) - - @responses.activate - def test_owner_receives_unfiltered_dataset_data(self): - ''' - The general public will have some fields filtered out from the dataset, - in order to protect sensitive data. The owner of a dataset however should - always receive full data. - ''' - self._set_cr_owner_to_token_user(1) - - def _check_fields(obj): - for sensitive_field in ['email', 'telephone', 'phone']: - self.assertEqual(sensitive_field in obj['research_dataset']['curator'][0], True, - 'field %s should be present' % sensitive_field) - - for cr in CatalogRecordV2.objects.filter(pk=1): - cr.research_dataset['curator'][0].update({ - 'email': 'email@mail.com', - 'phone': '123124', - 'telephone': '123124', - }) - cr.force_save() - - response = self.client.get('/rest/v2/datasets/1') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - _check_fields(response.data) + self.assertEqual(response.data["metadata_provider_user"], original) class CatalogRecordExternalServicesAccess(CatalogRecordApiWriteCommon): @@ -1897,111 +632,26 @@ def setUp(self): """ super().setUp() - self.dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema='att').first() - self.dc.catalog_json['identifier'] = EXT_CATALOG - self.dc.catalog_json['harvested'] = True - self.dc.catalog_record_services_create = 'external' - self.dc.catalog_record_services_edit = 'external' + self.dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema="att").first() + self.dc.catalog_json["identifier"] = EXT_CATALOG + self.dc.catalog_json["harvested"] = True + self.dc.catalog_record_services_create = "external" + self.dc.catalog_record_services_edit = "external" self.dc.force_save() - self.cr_test_data['data_catalog'] = self.dc.catalog_json['identifier'] - del self.cr_test_data['research_dataset']['files'] - del self.cr_test_data['research_dataset']['total_files_byte_size'] - - self._use_http_authorization(username=django_settings.API_EXT_USER['username'], - password=django_settings.API_EXT_USER['password']) - - def test_external_service_can_not_read_all_metadata_in_other_catalog(self): - ''' External service should get the same output from someone elses catalog than anonymous user ''' - # create a catalog that does not belong to our external service - dc2 = DataCatalog.objects.get(pk=2) - dc2.catalog_json['identifier'] = 'Some other catalog' - dc2.catalog_record_services_read = 'metax' - dc2.force_save() - - # Create a catalog record that belongs to some other user & our catalog nr2 - cr = CatalogRecordV2.objects.get(pk=12) - cr.user_created = '#### Some owner who is not you ####' - cr.metadata_provider_user = '#### Some owner who is not you ####' - cr.data_catalog = dc2 - cr.editor = None - cr.research_dataset['access_rights']['access_type']['identifier'] = ACCESS_TYPES['restricted'] - cr.force_save() - - # Let's try to return the data with our external services credentials - response_service_user = self.client.get('/rest/v2/datasets/12') - self.assertEqual(response_service_user.status_code, status.HTTP_200_OK, response_service_user.data) - - # Test access as unauthenticated user - self.client._credentials = {} - response_anonymous = self.client.get('/rest/v2/datasets/12') - self.assertEqual(response_anonymous.status_code, status.HTTP_200_OK, response_anonymous.data) + self.cr_test_data["data_catalog"] = self.dc.catalog_json["identifier"] + del self.cr_test_data["research_dataset"]["files"] + del self.cr_test_data["research_dataset"]["total_files_byte_size"] - self.assertEqual(response_anonymous.data, response_service_user.data, - "External service with no read-rights should not see any more metadata than anonymous user from a catalog") - - def test_external_service_can_add_catalog_record_to_own_catalog(self): - self.cr_test_data['research_dataset']['preferred_identifier'] = '123456' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], '123456') - - def test_external_service_can_update_catalog_record_in_own_catalog(self): - self.cr_test_data['research_dataset']['preferred_identifier'] = '123456' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], '123456') - - cr_id = response.data['id'] - self.cr_test_data['research_dataset']['preferred_identifier'] = '654321' - response = self.client.put('/rest/v2/datasets/{}'.format(cr_id), self.cr_test_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['research_dataset']['preferred_identifier'], '654321') - - def test_external_service_can_delete_catalog_record_from_own_catalog(self): - self.cr_test_data['research_dataset']['preferred_identifier'] = '123456' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - - cr_id = response.data['id'] - response = self.client.delete('/rest/v2/datasets/{}'.format(cr_id)) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - - response = self.client.get('/rest/v2/datasets/{}'.format(cr_id), format="json") - self.assertEqual('not found' in response.json()['detail'].lower(), True) + self._use_http_authorization( + username=django_settings.API_EXT_USER["username"], + password=django_settings.API_EXT_USER["password"], + ) def test_external_service_can_not_add_catalog_record_to_other_catalog(self): - dc = self._get_object_from_test_data('datacatalog', requested_index=1) - self.cr_test_data['data_catalog'] = dc['catalog_json']['identifier'] - self.cr_test_data['research_dataset']['preferred_identifier'] = 'temp-pid' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_external_service_can_not_update_catalog_record_in_other_catalog(self): - response = self.client.put('/rest/v2/datasets/1', {}, format="json") - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_external_service_can_not_delete_catalog_record_from_other_catalog(self): - response = self.client.delete('/rest/v2/datasets/1') - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + dc = self._get_object_from_test_data("datacatalog", requested_index=1) + self.cr_test_data["data_catalog"] = dc["catalog_json"]["identifier"] + self.cr_test_data["research_dataset"]["preferred_identifier"] = "temp-pid" + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") - def test_harvested_catalogs_must_have_preferred_identifier_create(self): - # create without preferred identifier - - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('must have preferred identifier' in - response.data['research_dataset']['preferred_identifier'][0], True) - - self.cr_test_data['research_dataset']['preferred_identifier'] = '' - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('must have preferred identifier' in - response.data['research_dataset']['preferred_identifier'][0], True) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/directories/read.py b/src/metax_api/tests/api/rest/v2/views/directories/read.py index f88925e3..3a20880f 100755 --- a/src/metax_api/tests/api/rest/v2/views/directories/read.py +++ b/src/metax_api/tests/api/rest/v2/views/directories/read.py @@ -6,426 +6,30 @@ # :license: MIT import copy -import responses from django.core.management import call_command -from django.db import transaction from django.db.models import Count, Sum from rest_framework import status from rest_framework.test import APITestCase -from metax_api.models import CatalogRecord, Directory, File -from metax_api.models.catalog_record import ACCESS_TYPES -from metax_api.tests.utils import TestClassUtils, get_test_oidc_token, test_data_file_path +from metax_api.models import CatalogRecord, Directory +from metax_api.tests.utils import TestClassUtils, test_data_file_path class DirectoryApiReadCommon(APITestCase, TestClassUtils): - @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(DirectoryApiReadCommon, cls).setUpClass() def setUp(self): - dir_from_test_data = self._get_object_from_test_data('directory') - self.identifier = dir_from_test_data['identifier'] - self.pk = dir_from_test_data['id'] + dir_from_test_data = self._get_object_from_test_data("directory") + self.identifier = dir_from_test_data["identifier"] + self.pk = dir_from_test_data["id"] self._use_http_authorization() - def _create_test_dirs(self, count): - count = count + 1 - with transaction.atomic(): - for n in range(1, count): - f = self._get_new_file_data(str(n)) - self.client.post('/rest/v2/files', f, format="json") - - def _get_dirs_files_ids(self, url): - file_data = self.client.get(url).data - if isinstance(file_data, dict): - return {key: [f['id'] for f in file_data[key]] for key in file_data.keys() - if key in ['directories', 'files']} - else: - return [f['id'] for f in file_data] - - def _get_new_file_data(self, file_n): - from_test_data = self._get_object_from_test_data('file', requested_index=0) - - path = '/prj_112_root/science_data_C/phase_2/2017/10/dir_' + file_n + '/file_' + file_n - identifier = 'urn:nbn:fi:100' + file_n - - from_test_data.update({ - "checksum": { - "value": "habeebit", - "algorithm": "SHA-256", - "checked": "2017-05-23T10:07:22.559656Z", - }, - "file_name": "tiedosto_name_" + file_n, - "file_path": path, - "identifier": identifier, - "file_storage": self._get_object_from_test_data('filestorage', requested_index=0), - 'parent_directory': 24, - 'project_identifier': 'research_project_112' - }) - del from_test_data['id'] - return from_test_data - - -class DirectoryApiReadBasicTests(DirectoryApiReadCommon): - - def test_read_directory_list(self): - response = self.client.get('/rest/v2/directories') - self.assertEqual(response.status_code, 501) - - def test_read_directory_details_by_pk(self): - response = self.client.get('/rest/v2/directories/%s' % self.pk) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(hasattr(response, 'data'), True, 'Request response object is missing attribute \'data\'') - self.assertEqual('directory_name' in response.data.keys(), True) - self.assertEqual(response.data['identifier'], self.identifier) - - def test_read_directory_details_by_identifier(self): - response = self.client.get('/rest/v2/directories/%s' % self.identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(hasattr(response, 'data'), True, 'Request response object is missing attribute \'data\'') - self.assertEqual('directory_name' in response.data.keys(), True) - self.assertEqual(response.data['identifier'], self.identifier) - - def test_read_directory_details_not_found(self): - response = self.client.get('/rest/v2/directories/shouldnotexist') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - -class DirectoryApiReadFileBrowsingTests(DirectoryApiReadCommon): - - """ - Test generic file browsing, should always return all existing files in a dir. - """ - - def test_read_directory_get_files(self): - """ - Test browsing files - """ - response = self.client.get('/rest/v2/directories/2/files') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data['directories'][0]['id'], 3) - self.assertEqual(response.data['directories'][0]['parent_directory']['id'], 2) - self.assertEqual(len(response.data['files']), 0) - - response = self.client.get('/rest/v2/directories/3/files') - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data['directories'][0]['id'], 4) - self.assertEqual(response.data['directories'][0]['parent_directory']['id'], 3) - self.assertEqual(len(response.data['files']), 5) - self.assertEqual(response.data['files'][0]['parent_directory']['id'], 3) - self.assertEqual(response.data['files'][4]['parent_directory']['id'], 3) - - response = self.client.get('/rest/v2/directories/4/files') - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data['directories'][0]['parent_directory']['id'], 4) - self.assertEqual(len(response.data['files']), 5) - self.assertEqual(response.data['files'][0]['parent_directory']['id'], 4) - self.assertEqual(response.data['files'][4]['parent_directory']['id'], 4) - - response = self.client.get('/rest/v2/directories/5/files') - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['files']), 0) - - response = self.client.get('/rest/v2/directories/6/files') - self.assertEqual(len(response.data['directories']), 0) - self.assertEqual(len(response.data['files']), 10) - self.assertEqual(response.data['files'][0]['parent_directory']['id'], 6) - self.assertEqual(response.data['files'][9]['parent_directory']['id'], 6) - - def test_read_directory_get_files_recursively(self): - """ - Test query parameter 'recursive'. - """ - - # without depth, returns from depth=1, which should contain no files - response = self.client.get('/rest/v2/directories/1/files?recursive') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data), 0) - - # dir id 1 (the root) contains 0 files, but recursively 20 - response = self.client.get('/rest/v2/directories/1/files?recursive=true&depth=*') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 20) - - # dir id 3 contains 5 files, but recursively 20 - response = self.client.get('/rest/v2/directories/3/files?recursive=true&depth=*') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 20) - - # dir id 4 contains 5 files, but recursively 15 - response = self.client.get('/rest/v2/directories/4/files?recursive=true&depth=*') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 15) - - # dir id 5 contains 0 files - response = self.client.get('/rest/v2/directories/5/files?recursive=true&depth=*') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 10) - - # dir id 6 contains 10 files - response = self.client.get('/rest/v2/directories/6/files?recursive=true&depth=*') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 10) - - def test_read_directory_get_files_file_not_found(self): - response = self.client.get('/rest/v2/directories/not_found/files') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_read_directory_get_project_root_directory(self): - response = self.client.get('/rest/v2/directories/root?project=project_x') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.content) - self.assertEqual(response.data['id'], 1) - self.assertEqual('directories' in response.data, True) - self.assertEqual('files' in response.data, True) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data['directories'][0]['id'], 2) - - def test_read_directory_get_project_root_directory_not_found(self): - response = self.client.get('/rest/v2/directories/root?project=project_xyz') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_read_directory_get_project_root_directory_parameter_missing(self): - response = self.client.get('/rest/v2/directories/root') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('required' in response.data['detail'][0], True, response.data) - - def test_read_directory_get_files_by_path(self): - dr = Directory.objects.get(pk=2) - response = self.client.get('/rest/v2/directories/files?path=%s&project=%s' % - (dr.directory_path, dr.project_identifier)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data['directories'][0]['id'], 3) - self.assertEqual(response.data['directories'][0]['parent_directory']['id'], 2) - self.assertEqual(len(response.data['files']), 0) - - def test_read_directory_get_files_by_path_not_found(self): - response = self.client.get('/rest/v2/directories/files?path=%s&project=%s' % - ('doesnotexist', 'doesnotexist')) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_read_directory_get_files_by_path_check_parameters(self): - response = self.client.get('/rest/v2/directories/files') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/v2/directories/files?path=something') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - response = self.client.get('/rest/v2/directories/files?project=something') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_read_directory_recursively_with_max_depth(self): - """ - Should return a flat list of files, three directories deep - """ - response = self.client.get('/rest/v2/directories/2/files?recursive=true&depth=3') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 10) - - def test_read_directory_recursively_with_dirs_only_and_max_depth(self): - """ - Should return a directory hierarchy, three directories deep, with no files at all. - """ - response = self.client.get('/rest/v2/directories/2/files?recursive=true&directories_only=true&depth=3') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('directories' in response.data, True) - self.assertEqual('directories' in response.data['directories'][0], True) - self.assertEqual('directories' in response.data['directories'][0]['directories'][0], True) - - def test_read_directory_recursively_with_no_depth(self): - """ - recursive=true with no depth specified should not return everything, but instead depth=1 - by default. - - Using parameter directories_only=true to easier count the depth. - """ - response = self.client.get('/rest/v2/directories/3/files?recursive=true&directories_only=true') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('directories' in response.data, True) - self.assertEqual('directories' in response.data['directories'][0], True) - - def test_read_directory_return_directories_only(self): - response = self.client.get('/rest/v2/directories/3/files?directories_only') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual('files' in response.data, False) - - def test_read_directory_with_include_parent(self): - response = self.client.get('/rest/v2/directories/3/files?include_parent') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['files']), 5) - self.assertEqual(response.data.get('id', None), 3) - - def test_read_directory_files_sorted_by_file_path(self): - response = self.client.get('/rest/v2/directories/3/files') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['files'][0]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_1') - self.assertEqual(response.data['files'][1]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_2') - self.assertEqual(response.data['files'][2]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_3') - - response = self.client.get('/rest/v2/directories/3/files?pagination&limit=2&offset=2') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['results']['files'][0]['file_path'], - '/project_x_FROZEN/Experiment_X/file_name_2') - self.assertEqual(response.data['results']['files'][1]['file_path'], - '/project_x_FROZEN/Experiment_X/file_name_3') - - response = self.client.get('/rest/v2/directories/3/files?cr_identifier=2') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['files'][0]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_3') - self.assertEqual(response.data['files'][1]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_4') - - response = self.client.get('/rest/v2/directories/3/files?recursive') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data[0]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_1') - self.assertEqual(response.data[1]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_2') - self.assertEqual(response.data[2]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_3') - - response = self.client.get('/rest/v2/directories/3/files?recursive&cr_identifier=2') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data[0]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_3') - self.assertEqual(response.data[1]['file_path'], '/project_x_FROZEN/Experiment_X/file_name_4') - - def test_read_directory_directories_sorted_by_directory_path(self): - - response = self.client.get('/rest/v2/directories/8/files') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['directories'][0]['directory_path'], - '/prj_112_root/other') - self.assertEqual(response.data['directories'][1]['directory_path'], - '/prj_112_root/random_folder', response.data) - self.assertEqual(response.data['directories'][2]['directory_path'], - '/prj_112_root/science_data_A') - self.assertEqual(response.data['directories'][3]['directory_path'], - '/prj_112_root/science_data_B') - self.assertEqual(response.data['directories'][4]['directory_path'], - '/prj_112_root/science_data_C') - - response = self.client.get('/rest/v2/directories/8/files?pagination&limit=2&offset=2') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['results']['directories'][0]['directory_path'], - '/prj_112_root/science_data_A') - self.assertEqual(response.data['results']['directories'][1]['directory_path'], - '/prj_112_root/science_data_B') - - response = self.client.get('/rest/v2/directories/8/files?directories_only') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['directories'][0]['directory_path'], - '/prj_112_root/other') - self.assertEqual(response.data['directories'][1]['directory_path'], - '/prj_112_root/random_folder', response.data) - self.assertEqual(response.data['directories'][2]['directory_path'], - '/prj_112_root/science_data_A') - self.assertEqual(response.data['directories'][3]['directory_path'], - '/prj_112_root/science_data_B') - self.assertEqual(response.data['directories'][4]['directory_path'], - '/prj_112_root/science_data_C') - - response = self.client.get('/rest/v2/directories/8/files?cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['directories'][0]['directory_path'], - '/prj_112_root/other') - self.assertEqual(response.data['directories'][1]['directory_path'], - '/prj_112_root/random_folder') - self.assertEqual(response.data['directories'][2]['directory_path'], - '/prj_112_root/science_data_A') - self.assertEqual(response.data['directories'][3]['directory_path'], - '/prj_112_root/science_data_B') - - -class DirectoryApiReadFileBrowsingRetrieveSpecificFieldsTests(DirectoryApiReadCommon): - - def test_retrieve_requested_directory_fields_only(self): - - response = self.client.get('/rest/v2/directories/3/files?directory_fields=identifier,directory_path') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertEqual('identifier' in response.data['directories'][0], True) - self.assertEqual('directory_path' in response.data['directories'][0], True) - - self._use_http_authorization(username='metax') - - response = self.client.get('/rest/v2/directories/17/files? \ - cr_identifier=13&directory_fields=directory_name&directories_only&recursive') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertTrue('directories' in response.data['directories'][0]) - self.assertTrue('directory_name' in response.data['directories'][0]) - self.assertFalse('id' in response.data['directories'][0]) - - def test_retrieve_directory_byte_size_and_file_count(self): - """ - There is some additional logic involved in retrieving byte_size and file_count, which warrants - targeted tests for just those fields. - """ - response = self.client.get('/rest/v2/directories/3/files?directory_fields=identifier,byte_size') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertEqual('identifier' in response.data['directories'][0], True) - self.assertEqual('byte_size' in response.data['directories'][0], True) - - response = self.client.get('/rest/v2/directories/3/files?directory_fields=identifier,file_count') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertEqual('identifier' in response.data['directories'][0], True) - self.assertEqual('file_count' in response.data['directories'][0], True) - - response = self.client.get( - '/rest/v2/directories/3/files?directory_fields=identifier,file_count&cr_identifier=3') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertEqual('identifier' in response.data['directories'][0], True) - self.assertEqual('file_count' in response.data['directories'][0], True) - - response = self.client.get( - '/rest/v2/directories/3/files?directory_fields=identifier,file_count¬_cr_identifier=2') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories'][0].keys()), 2) - self.assertEqual('identifier' in response.data['directories'][0], True) - self.assertEqual('file_count' in response.data['directories'][0], True) - - def test_retrieve_requested_file_fields_only(self): - response = self.client.get('/rest/v2/directories/3/files?file_fields=identifier,file_path') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['files'][0].keys()), 2) - self.assertEqual('identifier' in response.data['files'][0], True) - self.assertEqual('file_path' in response.data['files'][0], True) - - def test_retrieve_requested_file_and_directory_fields_only(self): - response = self.client.get('/rest/v2/directories/3/files?file_fields=identifier&directory_fields=id') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files'][0].keys()), 1) - self.assertEqual('identifier' in response.data['files'][0], True) - self.assertEqual(len(response.data['directories'][0].keys()), 1) - self.assertEqual('id' in response.data['directories'][0], True) - - def test_not_retrieving_not_allowed_directory_fields(self): - from metax_api.api.rest.base.serializers import DirectorySerializer, FileSerializer - - allowed_dir_fields = set(DirectorySerializer.Meta.fields) - allowed_file_fields = set(FileSerializer.Meta.fields) - - response = self.client.get('/rest/v2/directories/3/files?file_fields=parent,id&directory_fields=;;drop db;,id') - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(any(field in response.data['files'][0].keys() for field in allowed_file_fields)) - self.assertTrue(any(field in response.data['directories'][0].keys() for field in allowed_dir_fields)) - - response = self.client.get('/rest/v2/directories/3/files?file_fields=parent&directory_fields=or') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - response = self.client.get('/rest/v2/directories/3/files?file_fields=parent') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - response = self.client.get('/rest/v2/directories/3/files?directory_fields=or') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - class DirectoryApiReadCatalogRecordFileBrowsingTests(DirectoryApiReadCommon): @@ -436,178 +40,69 @@ class DirectoryApiReadCatalogRecordFileBrowsingTests(DirectoryApiReadCommon): """ def setUp(self): - self._set_http_authorization('service') - self.client.get('/rest/v2/directories/update_byte_sizes_and_file_counts') - self.client.get('/rest/v2/datasets/update_cr_directory_browsing_data') - - def test_read_directory_catalog_record_and_not_catalog_record_not_ok(self): - """ - Test query parameter 'cr_identifier' and 'not_cr_identifier' can not be queried together. - """ - response = self.client.get('/rest/v2/directories/3/files?cr_identifier=1¬_cr_identifier=2') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue("one query parameter of 'cr_identifier' and 'not_cr_identifier'" in response.data['detail'][0]) - - def test_read_directory_for_catalog_record(self): - """ - Test query parameter 'cr_identifier'. - """ - response = self.client.get('/rest/v2/directories/3/files?cr_identifier=%s' - % CatalogRecord.objects.get(pk=1).identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('directories' in response.data, True) - self.assertEqual('files' in response.data, True) - self.assertEqual(len(response.data['directories']), 0) - self.assertEqual(len(response.data['files']), 2) - for f in response.data['files']: - self.assertTrue(f['parent_directory']['id'], 1) - - def test_read_directory_for_not_catalog_record(self): - """ - Test query parameter 'not_cr_identifier'. - """ - response = self.client.get('/rest/v2/directories/3/files?not_cr_identifier=2') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 3, response.data) - for f in response.data['files']: - self.assertNotEqual(f['parent_directory']['id'], 2, response.data) - self.assertEqual(len(response.data['directories']), 1, response.data) - self.assertNotEqual(response.data['directories'][0]['parent_directory']['id'], 2) - - def test_read_directory_for_catalog_record_not_found(self): - """ - Not found cr_identifier should raise 400 instead of 404, which is raised when the - directory itself is not found. the error contains details about the 400. - """ - response = self.client.get('/rest/v2/directories/3/files?cr_identifier=notexisting') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_read_directory_for_not_catalog_record_not_found(self): - """ - Not found cr_identifier should raise 400 instead of 404, which is raised when the - directory itself is not found. the error contains details about the 400. - """ - response = self.client.get('/rest/v2/directories/3/files?not_cr_identifier=notexisting') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_read_directory_for_catalog_record_directory_does_not_exist(self): - """ - A directory may have files in a project, but those files did not necessarily exist - or were not selected for a specific CR. - """ - - # should be OK... - response = self.client.get('/rest/v2/directories/4/files') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['files']), 5) - - # ... but should not contain any files FOR THIS CR - response = self.client.get('/rest/v2/directories/4/files?cr_identifier=%s' - % CatalogRecord.objects.get(pk=1).identifier) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - # ... and should contain files ALL BUT THIS CR - response = self.client.get('/rest/v2/directories/4/files?not_cr_identifier=%s' - % CatalogRecord.objects.get(pk=1).identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['files']), 5) - - def test_read_directory_for_catalog_record_recursively(self): - """ - Test query parameters 'cr_identifier' with 'recursive'. - """ - response = self.client.get('/rest/v2/directories/1/files?recursive&cr_identifier=%s&depth=*' - % CatalogRecord.objects.get(pk=1).identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) - file_list = list(File.objects.filter(record__pk=1).values_list('id', flat=True)) - self.assertEqual(len(response.data), len(file_list)) - for f in response.data: - self.assertTrue(f['id'] in file_list) - - response = self.client.get('/rest/v2/directories/1/files?recursive&cr_identifier=1&depth=*&directories_only') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['directories'][0]['directories']), 1) - self.assertEqual(len(response.data['directories'][0]['directories'][0]['directories']), 0) - self.assertFalse(response.data.get('files')) - - # not found cr_identifier should raise 400 instead of 404, which is raised when the - # directory itself is not found. the error contains details about the 400 - response = self.client.get('/rest/v2/directories/1/files?recursive&cr_identifier=notexisting') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_read_directory_for_not_catalog_record_recursively(self): - """ - Test query parameters 'not_cr_identifier' with 'recursive'. - """ - file_recursive = self.client.get('/rest/v2/directories/1/files?recursive&depth=*').data - file_list = list(File.objects.filter(record__pk=1).values_list('id', flat=True)) - response = self.client.get('/rest/v2/directories/1/files?recursive&depth=*¬_cr_identifier=%s' - % CatalogRecord.objects.get(pk=1).identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data), len(file_recursive) - len(file_list)) - for f in response.data: - self.assertTrue(f['id'] not in file_list) - - # not found not_cr_identifier should raise 400 instead of 404, which is raised when the - # directory itself is not found. the error contains details about the 400 - response = self.client.get('/rest/v2/directories/1/files?recursive¬_cr_identifier=notexisting') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self._set_http_authorization("service") + self.client.get("/rest/v2/directories/update_byte_sizes_and_file_counts") + self.client.get("/rest/v2/datasets/update_cr_directory_browsing_data") def test_directory_byte_size_and_file_count(self): """ Test byte size and file count are calculated correctly for directories when browsing files in the context of a single record. """ + def _assert_dir_calculations(cr, dr): """ Assert directory numbers received from browsing-api matches what exists in the db when making a reasonably fool-proof query of files by directory path """ - self.assertEqual('byte_size' in dr, True) - self.assertEqual('file_count' in dr, True) + self.assertEqual("byte_size" in dr, True) + self.assertEqual("file_count" in dr, True) - byte_size = cr.files.filter(file_path__startswith='%s/' % dr['directory_path']) \ - .aggregate(Sum('byte_size'))['byte_size__sum'] + byte_size = cr.files.filter( + file_path__startswith="%s/" % dr["directory_path"] + ).aggregate(Sum("byte_size"))["byte_size__sum"] - file_count = cr.files.filter(file_path__startswith='%s/' % dr['directory_path']).count() + file_count = cr.files.filter(file_path__startswith="%s/" % dr["directory_path"]).count() - self.assertEqual(dr['byte_size'], byte_size, 'path: %s' % dr['directory_path']) - self.assertEqual(dr['file_count'], file_count, 'path: %s' % dr['directory_path']) + self.assertEqual(dr["byte_size"], byte_size, "path: %s" % dr["directory_path"]) + self.assertEqual(dr["file_count"], file_count, "path: %s" % dr["directory_path"]) # prepare a new test dataset which contains a directory from testdata, which contains a decent # qty of files and complexity - dr = Directory.objects.get(directory_path='/prj_112_root') - cr_data = self.client.get('/rest/v2/datasets/1?include_user_metadata').data - cr_data.pop('id') - cr_data.pop('identifier') - cr_data['research_dataset'].pop('preferred_identifier') - cr_data['research_dataset'].pop('files', None) - cr_data['research_dataset']['directories'] = [{ - 'identifier': dr.identifier, - 'title': 'test dir', - 'use_category': { 'identifier': 'outcome' } - }] - self._use_http_authorization(username='metax') - response = self.client.post('/rest/v2/datasets', cr_data, format='json') + dr = Directory.objects.get(directory_path="/prj_112_root") + cr_data = self.client.get("/rest/v2/datasets/1?include_user_metadata").data + cr_data.pop("id") + cr_data.pop("identifier") + cr_data["research_dataset"].pop("preferred_identifier") + cr_data["research_dataset"].pop("files", None) + cr_data["research_dataset"]["directories"] = [ + { + "identifier": dr.identifier, + "title": "test dir", + "use_category": {"identifier": "outcome"}, + } + ] + self._use_http_authorization(username="metax") + response = self.client.post("/rest/v2/datasets", cr_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr_data = response.data - cr = CatalogRecord.objects.get(pk=cr_data['id']) + cr = CatalogRecord.objects.get(pk=cr_data["id"]) # begin tests # test: browse the file api, and receive a list of sub-directories - response = self.client.get('/rest/v2/directories/%d/files?cr_identifier=%s' % (dr.id, cr.identifier)) + response = self.client.get( + "/rest/v2/directories/%d/files?cr_identifier=%s" % (dr.id, cr.identifier) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - for directory in response.data['directories']: + for directory in response.data["directories"]: _assert_dir_calculations(cr, directory) # test: browse with ?include_parent=true to get the dir directly that was added to the dataset - response = self.client.get('/rest/v2/directories/%d/files?cr_identifier=%s&include_parent' - % (dr.id, cr.identifier)) + response = self.client.get( + "/rest/v2/directories/%d/files?cr_identifier=%s&include_parent" % (dr.id, cr.identifier) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) _assert_dir_calculations(cr, response.data) @@ -615,38 +110,53 @@ def test_directory_byte_size_and_file_count_in_parent_directories(self): cr_id = 13 def _assertDirectoryData(id, parent_data): - response = self.client.get('/rest/v2/directories/%d/files?cr_identifier=%d&include_parent' % (id, cr_id)) + response = self.client.get( + "/rest/v2/directories/%d/files?cr_identifier=%d&include_parent" % (id, cr_id) + ) - self.assertEqual(response.data['byte_size'], parent_data[id][0], response.data['id']) - self.assertEqual(response.data['file_count'], parent_data[id][1]) + self.assertEqual(response.data["byte_size"], parent_data[id][0], response.data["id"]) + self.assertEqual(response.data["file_count"], parent_data[id][1]) - if response.data.get('parent_directory'): - return _assertDirectoryData(response.data['parent_directory']['id'], parent_data) + if response.data.get("parent_directory"): + return _assertDirectoryData(response.data["parent_directory"]["id"], parent_data) def _assertDirectoryData_not_cr_id(id, parent_data): - total_dir_res = self.client.get('/rest/v2/directories/%d' % id) - total_dir_size = (total_dir_res.data.get('byte_size', None), total_dir_res.data.get('file_count', None)) - - response = self.client.get('/rest/v2/directories/%s/files?not_cr_identifier=%d&include_parent' % - (id, cr_id)) - if response.data.get('id'): - self.assertEqual(response.data['byte_size'], total_dir_size[0] - parent_data[id][0]) - self.assertEqual(response.data['file_count'], total_dir_size[1] - parent_data[id][1]) - - if response.data.get('parent_directory', None): - return _assertDirectoryData_not_cr_id(response.data['parent_directory']['id'], parent_data) + total_dir_res = self.client.get("/rest/v2/directories/%d" % id) + total_dir_size = ( + total_dir_res.data.get("byte_size", None), + total_dir_res.data.get("file_count", None), + ) + + response = self.client.get( + "/rest/v2/directories/%s/files?not_cr_identifier=%d&include_parent" % (id, cr_id) + ) + if response.data.get("id"): + self.assertEqual(response.data["byte_size"], total_dir_size[0] - parent_data[id][0]) + self.assertEqual( + response.data["file_count"], total_dir_size[1] - parent_data[id][1] + ) + + if response.data.get("parent_directory", None): + return _assertDirectoryData_not_cr_id( + response.data["parent_directory"]["id"], parent_data + ) def _get_parent_dir_data_for_cr(id): - def _get_parents(pk): pk = Directory.objects.get(pk=pk).parent_directory_id if pk: pks.append(pk) _get_parents(pk) - cr_data = CatalogRecord.objects.get(pk=id).files.order_by('parent_directory_id').values_list( - 'parent_directory_id').annotate(Sum('byte_size'), Count('id')) - grouped_cr_data = {parent_id: [byte_size, file_count] for parent_id, byte_size, file_count in cr_data} + cr_data = ( + CatalogRecord.objects.get(pk=id) + .files.order_by("parent_directory_id") + .values_list("parent_directory_id") + .annotate(Sum("byte_size"), Count("id")) + ) + grouped_cr_data = { + parent_id: [byte_size, file_count] for parent_id, byte_size, file_count in cr_data + } drs = {} for dir in grouped_cr_data.keys(): @@ -666,715 +176,19 @@ def _get_parents(pk): # begin tests - cr = self.client.get('/rest/v2/datasets/%d?include_user_metadata&file_details&' - 'directory_fields=id,byte_size,file_count,parent_directory&' - 'file_fields=id,byte_size,parent_directory' % cr_id) + cr = self.client.get( + "/rest/v2/datasets/%d?include_user_metadata&file_details&" + "directory_fields=id,byte_size,file_count,parent_directory&" + "file_fields=id,byte_size,parent_directory" % cr_id + ) - dirs = [d['details']['id'] for d in cr.data['research_dataset'].get('directories', [])] + \ - [f['details']['parent_directory']['id'] for f in cr.data['research_dataset'].get('files', [])] + dirs = [d["details"]["id"] for d in cr.data["research_dataset"].get("directories", [])] + [ + f["details"]["parent_directory"]["id"] + for f in cr.data["research_dataset"].get("files", []) + ] parent_data = _get_parent_dir_data_for_cr(cr_id) for id in set(dirs): _assertDirectoryData(id, parent_data) - _assertDirectoryData_not_cr_id(id, parent_data) - - -class DirectoryApiReadCatalogRecordFileBrowsingAuthorizationTests(DirectoryApiReadCommon): - - """ - Test browsing files in the context of a specific CatalogRecord from authorization perspective - """ - - # THE OK TESTS - - def test_returns_ok_for_open_catalog_record_if_no_authorization(self): - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns dir files even without authorization - # for open catalog record - self._assert_ok(open_cr_json, 'no') - - def test_returns_ok_for_login_catalog_record_if_no_authorization(self): - login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(use_login_access_type=True) - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns dir files even without authorization - # for login catalog record - self._assert_ok(login_cr_json, 'no') - - def test_returns_ok_for_open_catalog_record_if_service_authorization(self): - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns dir files with service - # authorization for open catalog record - self._assert_ok(open_cr_json, 'service') - - def test_returns_ok_for_login_catalog_record_if_service_authorization(self): - login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(use_login_access_type=True) - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns dir files with service - # authorization for login catalog record - self._assert_ok(login_cr_json, 'service') - - @responses.activate - def test_returns_ok_for_open_catalog_record_if_owner_authorization(self): - self.create_end_user_data_catalogs() - open_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns dir files with owner authorization for - # owner-owned open catalog record - self._assert_ok(open_cr_json, 'owner') - - @responses.activate - def test_returns_ok_for_login_catalog_record_if_owner_authorization(self): - self.create_end_user_data_catalogs() - login_cr_json = self.get_open_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns dir files with owner authorization for - # owner-owned login_cr_json catalog record - self._assert_ok(login_cr_json, 'owner') - - def test_returns_ok_for_restricted_catalog_record_if_service_authorization(self): - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns dir files with service - # authorization for restricted catalog record - self._assert_ok(restricted_cr_json, 'service') - - @responses.activate - def test_returns_ok_for_restricted_catalog_record_if_owner_authorization(self): - self.create_end_user_data_catalogs() - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns dir files with owner authorization for - # owner-owned restricted catalog record - self._assert_ok(restricted_cr_json, 'owner') - - def test_returns_ok_for_embargoed_catalog_record_if_available_reached_and_no_authorization(self): - available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(True) - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns dir files without authorization - # for embargoed catalog record whose embargo date has been reached - self._assert_ok(available_embargoed_cr_json, 'no') - - # THE FORBIDDEN TESTS - - def test_returns_forbidden_for_restricted_catalog_record_if_no_authorization(self): - restricted_cr_json = self.get_restricted_cr_with_files_and_dirs_from_api_with_file_details() - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns forbidden without authorization - # for restricted catalog record - self._assert_forbidden(restricted_cr_json, 'no') - - def test_returns_forbidden_for_embargoed_catalog_record_if_available_not_reached_and_no_authorization(self): - not_available_embargoed_cr_json = self.get_embargoed_cr_with_files_and_dirs_from_api_with_file_details( - False) - - # Verify /rest/v2/directories//files?cr_identifier=cr_id returns forbidden without authorization - # for embargoed catalog record whose embargo date has not been reached - # Deactivate credentials - self._assert_forbidden(not_available_embargoed_cr_json, 'no') - - def _assert_forbidden(self, cr_json, credentials_type): - dir_id = cr_json['research_dataset']['directories'][0]['identifier'] - cr_id = cr_json['identifier'] - self._set_http_authorization(credentials_type) - response = self.client.get('/rest/v2/directories/{0}/files?cr_identifier={1}'.format(dir_id, cr_id)) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - response = self.client.get('/rest/v2/directories/{0}/files?not_cr_identifier={1}'.format(dir_id, cr_id)) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def _assert_ok(self, cr_json, credentials_type): - dir_file_amt = cr_json['research_dataset']['directories'][0]['details']['file_count'] - dir_id = cr_json['research_dataset']['directories'][0]['identifier'] - cr_id = cr_json['identifier'] - self._set_http_authorization(credentials_type) - response = self.client.get('/rest/v2/directories/{0}/files?cr_identifier={1}&recursive&depth=*' - .format(dir_id, cr_id)) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data), dir_file_amt) - - response = self.client.get('/rest/v2/directories/{0}/files?not_cr_identifier={1}&recursive&depth=*' - .format(dir_id, cr_id)) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data), 0) - - -class DirectoryApiReadQueryFiltersTogetherTests(DirectoryApiReadCommon): - """ - Test browsing files and directories with different filtering combinations: - return ok if should work together and not ok if on opposite. - - recursive : If directories_only=true is also specified, returns a hierarchial directory tree instead, of x depth. - - depth: Max depth of recursion. Value must be an integer > 0, or *. default value is 1. - - directories_only: Omit files entirely from the returned results. Use together with recursive=true and depth=x - to get a directory tree. - - include_parent: Includes the 'parent directory' of the contents being fetched in the results also - - cr_identifier: Browse only files that have been selected for that record. - Incompatible with `not_cr_identifier` parameter. - - not_cr_identifier: Browse only files that have not been selected for that record. - Incompatible with `cr_identifier` parameter. - - file_fields: Field names to retrieve for files - - directory_fields: Field names to retrieve for directories - - file_name: Substring search from file names - - directory_name: Substring search from directory names - - pagination - - offset - - limit - """ - - def test_browsing_directories_with_filters(self): - # directory filters including directories_only - response = self.client.get('/rest/v2/directories/17/files? \ - directories_only&include_parent&directory_fields=id&directory_name=phase') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # file filters are not suppose to break anything with directories_only - response = self.client.get('/rest/v2/directories/17/files? \ - directories_only&include_parent&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # adds cr_identifier - response = self.client.get('/rest/v2/directories/17/files? \ - directories_only&include_parent&cr_identifier=13&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # adds not_cr_identifier - response = self.client.get('/rest/v2/directories/17/files? \ - directories_only&include_parent¬_cr_identifier=13&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # cr_identifier and not_cr_identifier are NOT suppose to work together - response = self.client.get('/rest/v2/directories/17/files? \ - directories_only&include_parent&cr_identifier=11¬_cr_identifier=13& \ - directory_fields=id&directory_name=phase&file_fields=id&file_name=2') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - - # adds pagination - response = self.client.get('/rest/v2/directories/17/files? \ - directories_only&include_parent&cr_identifier=13&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2&pagination') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # adds recursive - response = self.client.get('/rest/v2/directories/17/files? \ - directories_only&include_parent&cr_identifier=13&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2&recursive&depth=*') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # adds recursive and pagination - response = self.client.get('/rest/v2/directories/17/files? \ - directories_only&include_parent&cr_identifier=13&directory_fields=id&directory_name=phase& \ - file_fields=id&file_name=2&recursive&depth=*') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_browsing_files_and_directories_with_filters(self): - # file filters - response = self.client.get('/rest/v2/directories/17/files? \ - include_parent&file_fields=id&file_name=file') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # adds directory filters - response = self.client.get('/rest/v2/directories/17/files? \ - include_parent&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # adds cr_identifier - response = self.client.get('/rest/v2/directories/17/files? \ - include_parent&cr_identifier=13&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # adds not_cr_identifier - response = self.client.get('/rest/v2/directories/17/files? \ - include_parent¬_cr_identifier=13&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # cr_identifier and not_cr_identifier are NOT suppose to work together - response = self.client.get('/rest/v2/directories/17/files? \ - include_parent&cr_identifier=13¬_cr_identifier=11&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - - # adds recursive, directory filters are not suppose to break anything - response = self.client.get('/rest/v2/directories/17/files? \ - include_parent&cr_identifier=13&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase&recursive&depth=*') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # adds pagination - response = self.client.get('/rest/v2/directories/17/files? \ - include_parent&cr_identifier=13&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase&pagination') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - # adds recursive and pagination - response = self.client.get('/rest/v2/directories/17/files? \ - include_parent&cr_identifier=13&file_fields=id&file_name=file& \ - directory_fields=id&directory_name=phase&recursive&depth=*&pagination') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - -class DirectoryApiReadCatalogRecordFileBrowsingRetrieveSpecificFieldsTests(DirectoryApiReadCommon): - - def setUp(self): - super().setUp() - CatalogRecord.objects.get(pk=12).calculate_directory_byte_sizes_and_file_counts() - - def test_retrieve_requested_directory_fields_only(self): - response = self.client.get( - '/rest/v2/datasets/12?include_user_metadata&file_details&directory_fields=identifier,directory_path' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['directories'][0]['details'].keys()), 2) - self.assertEqual('identifier' in response.data['research_dataset']['directories'][0]['details'], True) - self.assertEqual('directory_path' in response.data['research_dataset']['directories'][0]['details'], True) - - def test_retrieve_directory_byte_size_and_file_count(self): - """ - There is some additional logic involved in retrieving byte_size and file_count, which warrants - targeted tests for just those fields. - """ - response = self.client.get( - '/rest/v2/datasets/12?include_user_metadata&file_details&directory_fields=identifier,byte_size' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['directories'][0]['details'].keys()), 2) - self.assertEqual('identifier' in response.data['research_dataset']['directories'][0]['details'], True) - self.assertEqual('byte_size' in response.data['research_dataset']['directories'][0]['details'], True) - - response = self.client.get( - '/rest/v2/datasets/12?include_user_metadata&file_details&directory_fields=identifier,file_count' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['directories'][0]['details'].keys()), 2) - self.assertEqual('identifier' in response.data['research_dataset']['directories'][0]['details'], True) - self.assertEqual('file_count' in response.data['research_dataset']['directories'][0]['details'], True) - - def test_retrieve_requested_file_fields_only(self): - response = self.client.get( - '/rest/v2/datasets/12?include_user_metadata&file_details&file_fields=identifier,file_path' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['files'][0]['details'].keys()), 2) - self.assertEqual('identifier' in response.data['research_dataset']['files'][0]['details'], True) - self.assertEqual('file_path' in response.data['research_dataset']['files'][0]['details'], True) - - def test_retrieve_requested_file_and_directory_fields_only(self): - response = self.client.get( - '/rest/v2/datasets/12?include_user_metadata&file_details&file_fields=identifier&directory_fields=id' - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['files'][0]['details'].keys()), 1) - self.assertEqual('identifier' in response.data['research_dataset']['files'][0]['details'], True) - self.assertEqual(len(response.data['research_dataset']['directories'][0]['details'].keys()), 1) - self.assertEqual('id' in response.data['research_dataset']['directories'][0]['details'], True) - - -class DirectoryApiReadEndUserAccess(DirectoryApiReadCommon): - - ''' - Test End User Access permissions when browsing files using /rest/v2/directories api. - - Note: In these tests, the token by default does not have correct project groups. - Token project groups are only made valid by calling _update_token_with_project_of_directory(). - ''' - - def setUp(self): - super().setUp() - self.token = get_test_oidc_token() - self._mock_token_validation_succeeds() - - def _update_token_with_project_of_directory(self, dir_id): - proj = Directory.objects.get(pk=dir_id).project_identifier - self.token['group_names'].append('IDA01:%s' % proj) - self._use_http_authorization(method='bearer', token=self.token) - - @responses.activate - def test_user_can_browse_files_from_their_projects(self): - ''' - Ensure users can only read files from /rest/v2/directories owned by them. - ''' - self._use_http_authorization(method='bearer', token=self.token) - - # first read files without project access - should fail - response = self.client.get('/rest/v2/directories/1') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - response = self.client.get('/rest/v2/directories/1/files') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - # set user to same project as previous files and try again. should now succeed - self._update_token_with_project_of_directory(1) - - response = self.client.get('/rest/v2/directories/1') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/v2/directories/1/files') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - @responses.activate - def test_browsing_by_project_and_file_path_is_protected(self): - self._use_http_authorization(method='bearer', token=self.token) - - dr = Directory.objects.get(pk=2) - response = self.client.get('/rest/v2/directories/files?path=%s&project=%s' % - (dr.directory_path, dr.project_identifier)) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - self._update_token_with_project_of_directory(2) - response = self.client.get('/rest/v2/directories/files?path=%s&project=%s' % - (dr.directory_path, dr.project_identifier)) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - @responses.activate - def test_browsing_in_cr_context(self): - ''' - Cr with open access type should be available for any end-user api user. Browsing files for a cr with restricted - access type should be forbidden for non-owner (or service) user. - ''' - cr = CatalogRecord.objects.get(pk=1) - self._use_http_authorization(method='bearer', token=self.token) - response = self.client.get('/rest/v2/directories/3/files?cr_identifier={0}'.format(cr.identifier)) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - cr.research_dataset['access_rights']['access_type']['identifier'] = ACCESS_TYPES['restricted'] - cr.force_save() - - response = self.client.get('/rest/v2/directories/3/files?cr_identifier={0}'.format(cr.identifier)) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - @responses.activate - def test_browsing_in_not_cr_context(self): - ''' - Cr with open access type should be available for any end-user api user. Browsing files for a cr with restricted - access type should be forbidden for non-owner (or service) user. - ''' - cr = CatalogRecord.objects.get(pk=1) - self._use_http_authorization(method='bearer', token=self.token) - response = self.client.get('/rest/v2/directories/3/files?not_cr_identifier={0}'.format(cr.identifier)) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - cr.research_dataset['access_rights']['access_type']['identifier'] = ACCESS_TYPES['restricted'] - cr.force_save() - - response = self.client.get('/rest/v2/directories/3/files?not_cr_identifier={0}'.format(cr.identifier)) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - -class DirectoryApiReadPaginationTests(DirectoryApiReadCommon): - - """ - Test paginated directory and file browsing. - Should return directories and/or files depending on limit and offset parameters. Defaul is ofcet is 10 - """ - - def setUp(self): - self._use_http_authorization() - self._create_test_dirs(14) - - def test_read_directory_with_default_limit_pagination(self): - """ - Test browsing files with pagination - """ - file_dict = self._get_dirs_files_ids('/rest/v2/directories/24/files') - - response = self.client.get('/rest/v2/directories/24/files?pagination') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 10) - self.assertEqual(len(response.data['results']['files']), 0) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][0]) - self.assertEqual(response.data['results']['directories'][9]['id'], file_dict['directories'][9]) - - next_link = response.data['next'].split('http://testserver')[1] - response = self.client.get(next_link) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 4) - self.assertEqual(len(response.data['results']['files']), 6) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][10]) - self.assertEqual(response.data['results']['directories'][3]['id'], file_dict['directories'][13]) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][0]) - self.assertEqual(response.data['results']['files'][5]['id'], file_dict['files'][5]) - - next_link = response.data['next'].split('http://testserver')[1] - response = self.client.get(next_link) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 0) - self.assertEqual(len(response.data['results']['files']), 10) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][6]) - self.assertEqual(response.data['results']['files'][9]['id'], file_dict['files'][15]) - - prev_link = response.data['previous'].split('http://testserver')[1] - response = self.client.get(prev_link) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 4) - self.assertEqual(len(response.data['results']['files']), 6) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][10]) - self.assertEqual(response.data['results']['directories'][3]['id'], file_dict['directories'][13]) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][0]) - self.assertEqual(response.data['results']['files'][5]['id'], file_dict['files'][5]) - - def test_read_directory_with_custom_limit_pagination(self): - file_dict = self._get_dirs_files_ids('/rest/v2/directories/24/files') - - response = self.client.get('/rest/v2/directories/24/files?limit=4&offset=12&pagination') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 2) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][12]) - self.assertEqual(response.data['results']['directories'][1]['id'], file_dict['directories'][13]) - self.assertEqual(len(response.data['results']['files']), 2) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][0]) - self.assertEqual(response.data['results']['files'][1]['id'], file_dict['files'][1]) - - next_link = response.data['next'].split('http://testserver')[1] - prev_link = response.data['previous'].split('http://testserver')[1] - - response = self.client.get(next_link) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 0) - self.assertEqual(len(response.data['results']['files']), 4) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][2]) - self.assertEqual(response.data['results']['files'][3]['id'], file_dict['files'][5]) - - response = self.client.get(prev_link) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 4) - self.assertEqual(len(response.data['results']['files']), 0) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][8]) - self.assertEqual(response.data['results']['directories'][3]['id'], file_dict['directories'][11]) - - def test_read_directory_with_recursive_and_pagination(self): - ''' - Query with recursive flag must return only files as a list - ''' - file_list = self._get_dirs_files_ids('/rest/v2/directories/24/files?recursive') - - response = self.client.get('/rest/v2/directories/24/files?recursive&pagination') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 10) - self.assertEqual(response.data['results'][0]['id'], file_list[0]) - self.assertEqual(response.data['results'][9]['id'], file_list[9]) - - next_link = response.data['next'].split('http://testserver')[1] - response = self.client.get(next_link) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 10) - self.assertEqual(response.data['results'][0]['id'], file_list[10]) - self.assertEqual(response.data['results'][9]['id'], file_list[19]) - - prev_link = response.data['previous'].split('http://testserver')[1] - response = self.client.get(prev_link) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']), 10) - self.assertEqual(response.data['results'][0]['id'], file_list[0]) - self.assertEqual(response.data['results'][9]['id'], file_list[9]) - - def test_read_directory_with_dirs_only_and_pagination(self): - ''' - Query with directories_only flag must return only directories - ''' - file_dict = self._get_dirs_files_ids('/rest/v2/directories/24/files?directories_only')['directories'] - - response = self.client.get('/rest/v2/directories/24/files?directories_only&pagination=true') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['results']['directories']), 10) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict[0]) - self.assertEqual(response.data['results']['directories'][9]['id'], file_dict[9]) - - next_link = response.data['next'].split('http://testserver')[1] - response = self.client.get(next_link) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 4) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict[10]) - self.assertEqual(response.data['results']['directories'][3]['id'], file_dict[13]) - - prev_link = response.data['previous'].split('http://testserver')[1] - response = self.client.get(prev_link) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 10) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict[0]) - self.assertEqual(response.data['results']['directories'][9]['id'], file_dict[9]) - - def test_read_directory_with_parent_and_pagination(self): - ''' - Query with directories_only flag must return only directories - ''' - file_dict = self._get_dirs_files_ids('/rest/v2/directories/24/files?include_parent') - - response = self.client.get('/rest/v2/directories/24/files?include_parent&pagination=true') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 10) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][0]) - self.assertEqual(response.data['results']['directories'][9]['id'], file_dict['directories'][9]) - self.assertEqual(response.data['results']['id'], 24) - self.assertEqual(response.data['results']['directory_name'], "10") - - next_link = response.data['next'].split('http://testserver')[1] - response = self.client.get(next_link) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 4) - self.assertEqual(len(response.data['results']['files']), 6) - self.assertEqual(response.data['results']['directories'][0]['id'], file_dict['directories'][10]) - self.assertEqual(response.data['results']['directories'][3]['id'], file_dict['directories'][13]) - self.assertEqual(response.data['results']['files'][0]['id'], file_dict['files'][0]) - self.assertEqual(response.data['results']['files'][5]['id'], file_dict['files'][5]) - self.assertEqual(response.data['results']['id'], 24) - self.assertEqual(response.data['results']['directory_name'], "10") - - -class DirectoryApiReadFileNameDirectoryNameTests(DirectoryApiReadCommon): - - """ - Test browsing files with queries on file and directory names. - """ - - def setUp(self): - self._use_http_authorization() - self._create_test_dirs(5) - - def test_browsing_directory_with_file_name(self): - - response = self.client.get('/rest/v2/directories/24/files?file_name=') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['files']), 51) - - response = self.client.get('/rest/v2/directories/24/files?file_name=_name_1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['files']), 21) - - response = self.client.get('/rest/v2/directories/24/files?file_name=0') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['files']), 15) - - response = self.client.get('/rest/v2/directories/24/files?file_name=_name_118') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['files']), 1) - - def test_browsing_directory_with_directory_name(self): - - response = self.client.get('/rest/v2/directories/24/files?directory_name=') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 5) - - response = self.client.get('/rest/v2/directories/24/files?directory_name=dir_1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - - response = self.client.get('/rest/v2/directories/24/files?directory_name=dir') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 5) - - def test_browsing_directory_with_directory_and_file_name(self): - - response = self.client.get('/rest/v2/directories/24/files?directory_name=&file_name=') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 5) - self.assertEqual(len(response.data['files']), 51) - - response = self.client.get('/rest/v2/directories/24/files?directory_name=dir_1&file_name=file_name_120') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(len(response.data['files']), 1) - - response = self.client.get('/rest/v2/directories/24/files?directory_name=dir&file_name=not_existing') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 5) - self.assertEqual(len(response.data['files']), 0) - - def test_browsing_directory_with_file_and_dir_name_and_pagination(self): - # second page should return last filtered files - response = self.client.get('/rest/v2/directories/24/files?file_name=0&pagination&limit=10&offset=10') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 0) - self.assertEqual(len(response.data['results']['files']), 10) - - # first page with limit of 3 should return first filtered directories - response = self.client.get('/rest/v2/directories/24/files?directory_name=dir_&pagination&limit=3') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 3) - self.assertEqual(len(response.data['results']['files']), 0) - - # first page with limit of 3 should return first filtered directories - response = self.client.get('/rest/v2/directories/24/files?directory_name=dir_1&file_name=0&pagination') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['results']['directories']), 1) - self.assertEqual(len(response.data['results']['files']), 9) - - def test_browsing_directory_with_directory_and_file_name_and_dirs_only(self): - - response = self.client.get('/rest/v2/directories/24/files?file_name=_name_11&directories_only') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 5) - self.assertEqual(response.data.get('files'), None) - - response = self.client.get('/rest/v2/directories/24/files?directory_name=dir_5&directories_only') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['directories']), 1) - self.assertEqual(response.data.get('files'), None) - - def test_browsing_directory_with_directory_and_file_name_and_recursive(self): - - response = self.client.get('/rest/v2/directories/24/files?file_name=_name_11&recursive') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 10) - - # should have one file from directory with the rest of filtered files - response = self.client.get('/rest/v2/directories/24/files?directory_name=dir_5&file_name=5&recursive&depth=*') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 6) - - def test_browsing_directory_with_directory_and_file_name_and_cr_identifier_and_not_cr_identifier(self): - # tests for directory_name and cr_identifier - response = self.client.get('/rest/v2/directories/17/files?directory_name=2&cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 1) - - response = self.client.get('/rest/v2/directories/17/files?directory_name=phase&cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 2) - - response = self.client.get('/rest/v2/directories/17/files?directory_name=&cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 2) - - # tests for directory_name and not_cr_identifier - response = self.client.get('/rest/v2/directories/17/files?directory_name=phase¬_cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 0) - - response = self.client.get('/rest/v2/directories/17/files?directory_name=2¬_cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['directories']), 0) - - # tests for file_name and cr_identifier - response = self.client.get('/rest/v2/directories/12/files?file_name=22&cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 1) - - response = self.client.get('/rest/v2/directories/12/files?file_name=name_&cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 3) - - response = self.client.get('/rest/v2/directories/12/files?file_name=&cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 3) - - response = self.client.get('/rest/v2/directories/17/files?file_name=&cr_identifier=13&directories_only') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('files'), None) - - # tests for file_name and not_cr_identifier - response = self.client.get('/rest/v2/directories/16/files?file_name=name¬_cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 1) - - response = self.client.get('/rest/v2/directories/16/files?file_name=name_2¬_cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 1) - - response = self.client.get('/rest/v2/directories/16/files?file_name=name_1¬_cr_identifier=13') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['files']), 0) + _assertDirectoryData_not_cr_id(id, parent_data) \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/directories/write.py b/src/metax_api/tests/api/rest/v2/views/directories/write.py index e7460776..a7526984 100755 --- a/src/metax_api/tests/api/rest/v2/views/directories/write.py +++ b/src/metax_api/tests/api/rest/v2/views/directories/write.py @@ -20,17 +20,17 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(DirectoryApiWriteCommon, cls).setUpClass() def setUp(self): """ Reloaded for every test case """ - call_command('loaddata', test_data_file_path, verbosity=0) - dir_from_test_data = self._get_object_from_test_data('directory') - self.identifier = dir_from_test_data['identifier'] - self.directory_name = dir_from_test_data['directory_name'] + call_command("loaddata", test_data_file_path, verbosity=0) + dir_from_test_data = self._get_object_from_test_data("directory") + self.identifier = dir_from_test_data["identifier"] + self.directory_name = dir_from_test_data["directory_name"] """ New data that is sent to the server for POST, PUT, PATCH requests. Modified @@ -41,141 +41,195 @@ def setUp(self): self._use_http_authorization() def _get_new_test_data(self): - from_test_data = self._get_object_from_test_data('directory', requested_index=0) - from_test_data.update({ - "identifier": "urn:nbn:fi:csc-ida201401200000000001", - }) + from_test_data = self._get_object_from_test_data("directory", requested_index=0) + from_test_data.update( + { + "identifier": "urn:nbn:fi:csc-ida201401200000000001", + } + ) return from_test_data def _get_second_new_test_data(self): from_test_data = self._get_new_test_data() - from_test_data.update({ - "identifier": "urn:nbn:fi:csc-ida201401200000000002", - }) + from_test_data.update( + { + "identifier": "urn:nbn:fi:csc-ida201401200000000002", + } + ) return from_test_data class DirectoryApiWriteTests(DirectoryApiWriteCommon): - def test_create_files_for_catalog_record(self): """ Tests flow of creating files and assigning them to dataset. """ - project = 'project-test-files' - directory_path = '/dir/' + project = "project-test-files" + directory_path = "/dir/" files = [] for n in range(1, 4): - file_path = directory_path + 'file' + str(n) - f = self._get_new_file_data(str(n), project=project, file_path=file_path, - directory_path=directory_path, open_access=True) - f.pop('parent_directory', None) + file_path = directory_path + "file" + str(n) + f = self._get_new_file_data( + str(n), + project=project, + file_path=file_path, + directory_path=directory_path, + open_access=True, + ) + f.pop("parent_directory", None) files.append(f) cr = self._get_ida_dataset_without_files() - fields = 'file_fields=id,identifier,file_path&directory_fields=id,identifier,directory_path,file_count' + fields = "file_fields=id,identifier,file_path&directory_fields=id,identifier,directory_path,file_count" # start test # - self._set_http_authorization('service') + self._set_http_authorization("service") # adding file1 to /dir/ - response = self.client.post('/rest/v2/files', files[0], format='json') + response = self.client.post("/rest/v2/files", files[0], format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - file1_id = response.data['identifier'] + file1_id = response.data["identifier"] # adding file2 to /dir/ - response = self.client.post('/rest/v2/files', files[1], format='json') + response = self.client.post("/rest/v2/files", files[1], format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - file2_id = response.data['identifier'] + file2_id = response.data["identifier"] # adding file3 to /dir/ - response = self.client.post('/rest/v2/files', files[2], format='json') + response = self.client.post("/rest/v2/files", files[2], format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - file3_id = response.data['identifier'] + file3_id = response.data["identifier"] # creating dataset - response = self.client.post('/rest/v2/datasets?draft=true', cr, format='json') + response = self.client.post("/rest/v2/datasets?draft=true", cr, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] + cr_id = response.data["id"] # getting dataset root directory identifier (%2F=='/') - root_dir = self.client.get('/rest/v2/directories/files?project={}&path=%2F&include_parent'.format(project)) - root_id = root_dir.data['id'] + root_dir = self.client.get( + "/rest/v2/directories/files?project={}&path=%2F&include_parent".format(project) + ) + root_id = root_dir.data["id"] # getting dataset files from / - response = self.client.get('/rest/v2/directories/files?cr_identifier={}&project={}&path=%2F&{}' - .format(cr_id, project, fields)) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, 'Directory must be empty') + response = self.client.get( + "/rest/v2/directories/files?cr_identifier={}&project={}&path=%2F&{}".format( + cr_id, project, fields + ) + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, "Directory must be empty") # adding file1 to dataset - first_file = { - 'files': [ - {"identifier": file1_id} - ]} + first_file = {"files": [{"identifier": file1_id}]} - response = self.client.post('/rest/v2/datasets/{}/files'.format(cr_id), first_file, format='json') + response = self.client.post( + "/rest/v2/datasets/{}/files".format(cr_id), first_file, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # getting dataset files from / - response = self.client.get('/rest/v2/directories/{}/files?cr_identifier={}&fields'.format(root_id, cr_id)) + response = self.client.get( + "/rest/v2/directories/{}/files?cr_identifier={}&fields".format(root_id, cr_id) + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - dirs = response.data['directories'] - self.assertEqual(len(dirs), 1, 'Expected 1 directory') - self.assertEqual(dirs[0]['file_count'], 1, 'Expected 1 file in directory %s' % dirs[0]['directory_path']) + dirs = response.data["directories"] + self.assertEqual(len(dirs), 1, "Expected 1 directory") + self.assertEqual( + dirs[0]["file_count"], + 1, + "Expected 1 file in directory %s" % dirs[0]["directory_path"], + ) # getting dataset files from /dir/ - response = self.client.get('/rest/v2/directories/{}/files?cr_identifier={}&include_parent&{}' - .format(dirs[0]['id'], cr_id, fields)) - self.assertEqual(len(response.data['files']), 1, 'Expected 1 file in directory {}' - .format(dirs[0]['directory_path'])) - self.assertEqual(response.data['file_count'], len(response.data['files']), - 'Expected 1 file in parent file_count') + response = self.client.get( + "/rest/v2/directories/{}/files?cr_identifier={}&include_parent&{}".format( + dirs[0]["id"], cr_id, fields + ) + ) + self.assertEqual( + len(response.data["files"]), + 1, + "Expected 1 file in directory {}".format(dirs[0]["directory_path"]), + ) + self.assertEqual( + response.data["file_count"], + len(response.data["files"]), + "Expected 1 file in parent file_count", + ) # getting non-dataset files from /dir/ - response = self.client.get('/rest/v2/directories/{}/files?not_cr_identifier={}&include_parent&{}' - .format(dirs[0]['id'], cr_id, fields)) - self.assertEqual(len(response.data['files']), 2, 'Expected 2 files in directory {}' - .format(dirs[0]['directory_path'])) - self.assertEqual(response.data['file_count'], len(response.data['files']), - 'Expected 2 file in parent file_count') + response = self.client.get( + "/rest/v2/directories/{}/files?not_cr_identifier={}&include_parent&{}".format( + dirs[0]["id"], cr_id, fields + ) + ) + self.assertEqual( + len(response.data["files"]), + 2, + "Expected 2 files in directory {}".format(dirs[0]["directory_path"]), + ) + self.assertEqual( + response.data["file_count"], + len(response.data["files"]), + "Expected 2 file in parent file_count", + ) # adding file2 and file3 to dataset - last_files = { - 'files': [ - {'identifier': file2_id}, - {'identifier': file3_id} - ]} + last_files = {"files": [{"identifier": file2_id}, {"identifier": file3_id}]} - response = self.client.post('/rest/v2/datasets/{}/files'.format(cr_id), last_files, format='json') + response = self.client.post( + "/rest/v2/datasets/{}/files".format(cr_id), last_files, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # getting dataset files from /dir/ - response = self.client.get('/rest/v2/directories/{}/files?cr_identifier={}&include_parent&{}' - .format(dirs[0]['id'], cr_id, fields)) - self.assertEqual(len(response.data['files']), 3, 'Expected 3 files in directory {}' - .format(dirs[0]['directory_path'])) - self.assertEqual(response.data['file_count'], len(response.data['files']), - 'Expected 3 file in parent file_count') + response = self.client.get( + "/rest/v2/directories/{}/files?cr_identifier={}&include_parent&{}".format( + dirs[0]["id"], cr_id, fields + ) + ) + self.assertEqual( + len(response.data["files"]), + 3, + "Expected 3 files in directory {}".format(dirs[0]["directory_path"]), + ) + self.assertEqual( + response.data["file_count"], + len(response.data["files"]), + "Expected 3 file in parent file_count", + ) # getting non-dataset files from /dir/ - response = self.client.get('/rest/v2/directories/{}/files?not_cr_identifier={}&include_parent&{}' - .format(dirs[0]['id'], cr_id, fields)) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, 'Directory must be empty') + response = self.client.get( + "/rest/v2/directories/{}/files?not_cr_identifier={}&include_parent&{}".format( + dirs[0]["id"], cr_id, fields + ) + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, "Directory must be empty") # getting dataset files from / - response = self.client.get('/rest/v2/directories/{}/files?cr_identifier={}&fields'.format(root_id, cr_id)) + response = self.client.get( + "/rest/v2/directories/{}/files?cr_identifier={}&fields".format(root_id, cr_id) + ) self.assertEqual(response.status_code, status.HTTP_200_OK) - dirs = response.data['directories'] - self.assertEqual(len(dirs), 1, 'Expected 1 directory') - self.assertEqual(dirs[0]['file_count'], 3, 'Expected 3 files in directory %s' % dirs[0]['directory_path']) + dirs = response.data["directories"] + self.assertEqual(len(dirs), 1, "Expected 1 directory") + self.assertEqual( + dirs[0]["file_count"], + 3, + "Expected 3 files in directory %s" % dirs[0]["directory_path"], + ) # getting dataset files from / - response = self.client.get('/rest/v2/directories/{}/files?not_cr_identifier={}&fields'.format(root_id, cr_id)) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, 'Directory must be empty') + response = self.client.get( + "/rest/v2/directories/{}/files?not_cr_identifier={}&fields".format(root_id, cr_id) + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, "Directory must be empty") diff --git a/src/metax_api/tests/api/rest/v2/views/files/__init__.py b/src/metax_api/tests/api/rest/v2/views/files/__init__.py index 6adb6e69..d60060fb 100755 --- a/src/metax_api/tests/api/rest/v2/views/files/__init__.py +++ b/src/metax_api/tests/api/rest/v2/views/files/__init__.py @@ -5,5 +5,4 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from .read import * from .write import * diff --git a/src/metax_api/tests/api/rest/v2/views/files/read.py b/src/metax_api/tests/api/rest/v2/views/files/read.py deleted file mode 100755 index f10cc32f..00000000 --- a/src/metax_api/tests/api/rest/v2/views/files/read.py +++ /dev/null @@ -1,327 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -import responses -from django.core.management import call_command -from django.db import connection -from rest_framework import status -from rest_framework.test import APITestCase - -from metax_api.models import File -from metax_api.tests.utils import TestClassUtils, get_test_oidc_token, test_data_file_path - - -class FileApiReadCommon(APITestCase, TestClassUtils): - @classmethod - def setUpClass(cls): - """ - Loaded only once for test cases inside this class. - """ - call_command('loaddata', test_data_file_path, verbosity=0) - super(FileApiReadCommon, cls).setUpClass() - - def setUp(self): - file_from_test_data = self._get_object_from_test_data('file') - self.identifier = file_from_test_data['identifier'] - self.pk = file_from_test_data['id'] - self._use_http_authorization() - - -class FileApiReadBasicTests(FileApiReadCommon): - - def test_read_file_list(self): - response = self.client.get('/rest/v2/files') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_read_file_list_filter_by_project(self): - proj = File.objects.get(pk=1).project_identifier - file_count = File.objects.filter(project_identifier=proj).count() - response = self.client.get('/rest/v2/files?project_identifier=%s' % proj) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], file_count) - - def test_read_file_list_filter_by_project_and_path(self): - proj = File.objects.get(pk=1).project_identifier - path = "/project_x_FROZEN/Experiment_X/Phase_1/2017/01" - file_count = File.objects.filter(project_identifier=proj, file_path__contains=path).count() - response = self.client.get('/rest/v2/files?project_identifier=%s&file_path=%s' % (proj, path)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], file_count) - - # missing project_identifier - response = self.client.get('/rest/v2/files?file_path=%s' % path) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_read_file_details_by_pk(self): - response = self.client.get('/rest/v2/files/%s' % self.pk) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(hasattr(response, 'data'), True, 'Request response object is missing attribute \'data\'') - self.assertEqual('file_name' in response.data, True) - self.assertEqual(response.data['identifier'], self.identifier) - self.assertEqual('identifier' in response.data['file_storage'], True) - - def test_read_file_details_by_identifier(self): - response = self.client.get('/rest/v2/files/%s' % self.identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(hasattr(response, 'data'), True, 'Request response object is missing attribute \'data\'') - self.assertEqual('file_name' in response.data.keys(), True) - self.assertEqual(response.data['identifier'], self.identifier) - - def test_read_file_details_not_found(self): - response = self.client.get('/rest/v2/files/shouldnotexist') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_read_file_details_checksum_relation(self): - response = self.client.get('/rest/v2/files/%s' % self.pk) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('checksum' in response.data, True) - self.assertEqual('value' in response.data['checksum'], True) - - def test_expand_relations(self): - response = self.client.get('/rest/v2/files/1?expand_relation=file_storage,parent_directory') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('file_storage_json' in response.data['file_storage'], True, response.data['file_storage']) - self.assertEqual('date_created' in response.data['parent_directory'], True, response.data['parent_directory']) - - -class FileApiReadGetRelatedDatasets(FileApiReadCommon): - - def test_get_related_datasets_ok_1(self): - """ - File pk 1 should belong to only 3 datasets - """ - response = self.client.post('/rest/v2/files/datasets', [1], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 3) - - def test_get_related_datasets_ok_2(self): - """ - File identifiers listed below should belong to 5 datasets - """ - file_identifiers = File.objects.filter(id__in=[1, 2, 3, 4, 5]).values_list('identifier', flat=True) - response = self.client.post('/rest/v2/files/datasets', file_identifiers, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 5) - - def test_keysonly(self): - """ - Parameter ?keysonly should return just values - """ - response = self.client.post('/rest/v2/files/datasets?keys=files&keysonly', [1, 2, 121], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 2) # pid:urn:121 does not belong to any dataset - self.assertEqual(type(response.data), list, type(response.data)) # no dict keys - - response = self.client.post('/rest/v2/files/datasets?keys=files&keysonly=false', [1, 2], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(type(response.data), dict, response.data) # Return by keys - - response = self.client.post('/rest/v2/files/datasets?keys=datasets&keysonly', [1, 2, 14], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 2) # Only datasets 1 and 2 have files - self.assertEqual(type(response.data), list, type(response.data)) # no dict keys - - def test_get_detailed_related_datasets_ok_1(self): - """ - File identifiers listed below should belong to 3 datasets - """ - response = self.client.post('/rest/v2/files/datasets?keys=files', [1], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 1) - self.assertEqual(len(list(response.data.values())[0]), 3, response.data) - - # Support for ?detailed - response = self.client.post('/rest/v2/files/datasets?detailed', [1], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 1) - self.assertEqual(len(list(response.data.values())[0]), 3, response.data) - - def test_get_detailed_related_datasets_ok_2(self): - """ - File identifiers listed below should belong to 5 datasets - """ - file_identifiers = [1, 2, 3, 4, 5] - - response = self.client.post('/rest/v2/files/datasets?keys=files', file_identifiers, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 5) - - # set of all returned datasets - self.assertEqual(len(set(sum(response.data.values(), []))), 5, response.data) - - # check if identifiers work - file_identifiers = ['pid:urn:1', 'pid:urn:2', 'pid:urn:3', 'pid:urn:4', 'pid:urn:5'] - - response = self.client.post('/rest/v2/files/datasets?keys=files', file_identifiers, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 5) - - # set of all returned datasets - self.assertEqual(len(set(sum(response.data.values(), []))), 5, response.data) - - def test_get_detailed_related_files_ok_1(self): - """ - Dataset identifiers listed below should have 2 files - """ - response = self.client.post('/rest/v2/files/datasets?keys=datasets', [1], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 1) - self.assertEqual(len(list(response.data.values())[0]), 2, response.data) - - def test_get_detailed_related_files_ok_2(self): - """ - Tests that datasets return files correctly - """ - dataset_identifiers = [1, 2, 3, 4, 5] - - response = self.client.post('/rest/v2/files/datasets?keys=datasets', dataset_identifiers, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 5) - - # set of all returned datasets - self.assertEqual(len(set(sum(response.data.values(), []))), 10, response.data) - - # check if identifiers work - dataset_identifiers = ["cr955e904-e3dd-4d7e-99f1-3fed446f96d1", - "cr955e904-e3dd-4d7e-99f1-3fed446f96d2", - "cr955e904-e3dd-4d7e-99f1-3fed446f96d3", - "cr955e904-e3dd-4d7e-99f1-3fed446f96d4", - "cr955e904-e3dd-4d7e-99f1-3fed446f96d5"] - - response = self.client.post('/rest/v2/files/datasets?keys=datasets', dataset_identifiers, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 5) - - # set of all returned datasets - self.assertEqual(len(set(sum(response.data.values(), []))), 10, response.data) - - def test_get_right_files_and_datasets(self): - """ - Check that returned files and datasets are the right ones - """ - testfile = self._get_object_from_test_data('file') - - cr = self.client.get('/rest/v2/datasets/10', format='json') - self.assertEqual(cr.status_code, status.HTTP_200_OK, cr.data) - - response = self.client.post('/rest/v2/files/datasets?keys=datasets', [cr.data['identifier']], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - # cr 10 has 2 default files - for keys, values in response.data.items(): - self.assertEqual(keys == 'cr955e904-e3dd-4d7e-99f1-3fed446f9610', True, response.data) - self.assertEqual('pid:urn:19' and 'pid:urn:20' in values, True, response.data) - - response = self.client.post('/rest/files/datasets?keys=files', [testfile['identifier']], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - # file 1 belongs to 3 datasets - for keys, values in response.data.items(): - self.assertEqual(keys == 'pid:urn:1', True, response.data) - self.assertEqual('cr955e904-e3dd-4d7e-99f1-3fed446f96d1' and 'cr955e904-e3dd-4d7e-99f1-3fed446f9612' - and 'cr955e904-e3dd-4d7e-99f1-3fed446f9611' in values, True, response.data) - - # Dataset 11 has 20 files in a directory - cr = self.client.get('/rest/v2/datasets/11', format='json') - self.assertEqual(cr.status_code, status.HTTP_200_OK, cr.data) - - # Compare using return from different api - files_in_cr11 = self.client.get('/rest/v2/datasets/11/files', format='json') - self.assertEqual(files_in_cr11.status_code, status.HTTP_200_OK, files_in_cr11.data) - identifiers = [] - [identifiers.append(i['identifier']) for i in files_in_cr11.data] - - response = self.client.post('/rest/v2/files/datasets?keys=datasets', [11], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - # This should have the same file id's as the return from /rest/v2/datasets/11/files - self.assertEqual(sorted(response.data['cr955e904-e3dd-4d7e-99f1-3fed446f9611']), sorted(identifiers), - response.data) - - response = self.client.post('/rest/v2/files/datasets?keys=files', ['pid:urn:20'], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - # Dataset 11 should be found from results - self.assertTrue('cr955e904-e3dd-4d7e-99f1-3fed446f9611' in response.data['pid:urn:20'], response.data) - - def test_get_related_datasets_files_not_found(self): - """ - When the files themselves are not found, 404 should be returned - """ - response = self.client.post('/rest/v2/files/datasets', ['doesnotexist'], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 0) - - response = self.client.post('/rest/v2/files/datasets?keys=files', ['doesnotexist'], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 0) - - # Support for ?detailed - response = self.client.post('/rest/v2/files/datasets?detailed', ['doesnotexist'], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 0) - - def test_get_related_datasets_records_not_found(self): - """ - When files are found, but no records for them, an empty list should be returned - """ - with connection.cursor() as cr: - # detach file pk 1 from any datasets - cr.execute('delete from metax_api_catalogrecord_files where file_id = 1') - - response = self.client.post('/rest/v2/files/datasets', [1], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 0) - - response = self.client.post('/rest/v2/files/datasets?keys=files', [1], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 0) - - # Support for ?detailed - response = self.client.post('/rest/v2/files/datasets?detailed', [1], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self._assert_results_length(response, 0) - - def _assert_results_length(self, response, length): - self.assertTrue(isinstance(response.data, dict) or isinstance(response.data, list), response.data) - self.assertEqual(len(response.data), length) - - -class FileApiReadEndUserAccess(FileApiReadCommon): - - def setUp(self): - super().setUp() - self.token = get_test_oidc_token() - self._mock_token_validation_succeeds() - - @responses.activate - def test_user_can_read_owned_files(self): - ''' - Ensure users can only read files owned by them from /rest/v2/files api. - ''' - - # first read files without project access - should fail - self._use_http_authorization(method='bearer', token=self.token) - proj = File.objects.get(pk=1).project_identifier - - response = self.client.get('/rest/v2/files/1') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - response = self.client.get('/rest/v2/files?project_identifier=%s' % proj) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - response = self.client.get('/rest/files?pagination=false') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data), 0, 'should return 200 OK, but user projects has no files') - - # set user to same project as previous files and try again. should now succeed - self.token['group_names'].append('IDA01:%s' % proj) - self._use_http_authorization(method='bearer', token=self.token) - - response = self.client.get('/rest/v2/files') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data) > 0, True, 'user should only see their own files') - - response = self.client.get('/rest/v2/files/1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - response = self.client.get('/rest/v2/files?project_identifier=%s' % proj) - self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/metax_api/tests/api/rest/v2/views/files/write.py b/src/metax_api/tests/api/rest/v2/views/files/write.py index 4d5ddd64..96c5484f 100755 --- a/src/metax_api/tests/api/rest/v2/views/files/write.py +++ b/src/metax_api/tests/api/rest/v2/views/files/write.py @@ -5,18 +5,12 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from copy import deepcopy -from os.path import dirname - -import responses from django.core.management import call_command -from django.db.models import Sum from rest_framework import status from rest_framework.test import APITestCase -from metax_api.models import CatalogRecord, Directory, File from metax_api.services.redis_cache_service import RedisClient -from metax_api.tests.utils import TestClassUtils, get_test_oidc_token, test_data_file_path +from metax_api.tests.utils import TestClassUtils, test_data_file_path class FileApiWriteCommon(APITestCase, TestClassUtils): @@ -25,18 +19,18 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(FileApiWriteCommon, cls).setUpClass() def setUp(self): """ Reloaded for every test case """ - call_command('loaddata', test_data_file_path, verbosity=0) - file_from_test_data = self._get_object_from_test_data('file') - self.identifier = file_from_test_data['identifier'] - self.pidentifier = file_from_test_data['project_identifier'] - self.file_name = file_from_test_data['file_name'] + call_command("loaddata", test_data_file_path, verbosity=0) + file_from_test_data = self._get_object_from_test_data("file") + self.identifier = file_from_test_data["identifier"] + self.pidentifier = file_from_test_data["project_identifier"] + self.file_name = file_from_test_data["file_name"] """ New data that is sent to the server for POST, PUT, PATCH requests. Modified @@ -47,21 +41,25 @@ def setUp(self): self._use_http_authorization() def _get_new_test_data(self): - from_test_data = self._get_object_from_test_data('file', requested_index=0) - from_test_data.update({ - "checksum": { - "value": "habeebit", - "algorithm": "SHA-256", - "checked": "2017-05-23T10:07:22.559656Z", - }, - "file_name": "file_name_1", - "file_path": from_test_data['file_path'].replace('/some/path', '/some/other_path'), - "identifier": "urn:nbn:fi:csc-ida201401200000000001", - "file_storage": self._get_object_from_test_data('filestorage', requested_index=0) - }) - from_test_data['file_path'] = from_test_data['file_path'].replace('/Experiment_X/', '/test/path/') - from_test_data['project_identifier'] = 'test_project_identifier' - del from_test_data['id'] + from_test_data = self._get_object_from_test_data("file", requested_index=0) + from_test_data.update( + { + "checksum": { + "value": "habeebit", + "algorithm": "SHA-256", + "checked": "2017-05-23T10:07:22.559656Z", + }, + "file_name": "file_name_1", + "file_path": from_test_data["file_path"].replace("/some/path", "/some/other_path"), + "identifier": "urn:nbn:fi:csc-ida201401200000000001", + "file_storage": self._get_object_from_test_data("filestorage", requested_index=0), + } + ) + from_test_data["file_path"] = from_test_data["file_path"].replace( + "/Experiment_X/", "/test/path/" + ) + from_test_data["project_identifier"] = "test_project_identifier" + del from_test_data["id"] return from_test_data def _get_second_new_test_data(self): @@ -70,47 +68,24 @@ def _get_second_new_test_data(self): self._change_file_path(from_test_data, "file_name_2") return from_test_data - def _count_dirs_from_path(self, file_path): - expected_dirs_count = 1 - dir_name = dirname(file_path) - while dir_name != '/': - dir_name = dirname(dir_name) - expected_dirs_count += 1 - return expected_dirs_count - - def _check_project_root_byte_size_and_file_count(self, project_identifier): - """ - A rather simple test to fetch the root directory of a project, and verify that the - root's calculated total byte size and file count match what exists in the db. - """ - byte_size = File.objects.filter(project_identifier=project_identifier) \ - .aggregate(Sum('byte_size'))['byte_size__sum'] - file_count = File.objects.filter(project_identifier=project_identifier).count() - - response = self.client.get('/rest/v2/directories/root?project=%s' % project_identifier) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['byte_size'], byte_size) - self.assertEqual(response.data['file_count'], file_count) - def _change_file_path(self, file, new_name): - file['file_path'] = file['file_path'].replace(file['file_name'], new_name) - file['file_name'] = new_name + file["file_path"] = file["file_path"].replace(file["file_name"], new_name) + file["file_name"] = new_name class FileApiWriteReferenceDataValidationTests(FileApiWriteCommon): - @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('updatereferencedata', verbosity=0) + call_command("updatereferencedata", verbosity=0) super(FileApiWriteReferenceDataValidationTests, cls).setUpClass() def setUp(self): super().setUp() cache = RedisClient() - ffv_refdata = cache.get('reference_data')['reference_data']['file_format_version'] + ffv_refdata = cache.get("reference_data")["reference_data"]["file_format_version"] # File format version entry in reference data that has some output_format_version self.ff_with_version = None @@ -122,1300 +97,34 @@ def setUp(self): for ffv_obj in ffv_refdata: if self.ff_with_different_version is None and self.ff_with_version is not None: - if ffv_obj['input_file_format'] == self.ff_with_version['input_file_format']: + if ffv_obj["input_file_format"] == self.ff_with_version["input_file_format"]: self.ff_with_different_version = ffv_obj - if self.ff_with_version is None and ffv_obj['output_format_version']: + if self.ff_with_version is None and ffv_obj["output_format_version"]: self.ff_with_version = ffv_obj - if self.ff_without_version is None and not ffv_obj['output_format_version']: + if self.ff_without_version is None and not ffv_obj["output_format_version"]: self.ff_without_version = ffv_obj - self.assertTrue(self.ff_with_version['output_format_version'] != '') - self.assertTrue(self.ff_with_different_version['output_format_version'] != '') - self.assertTrue(self.ff_with_version['input_file_format'] == - self.ff_with_different_version['input_file_format']) - self.assertTrue(self.ff_with_version['output_format_version'] != - self.ff_with_different_version['output_format_version']) - self.assertTrue(self.ff_without_version['output_format_version'] == '') - - def test_file_format_version_with_invalid_file_format_when_format_version_given_1(self): - self.test_new_data['file_characteristics']['format_version'] = 'any' - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_characteristics' in response.data.keys(), True) - self.assertEqual('file_characteristics.file_format' in response.data['file_characteristics'], True) - - def test_file_format_version_with_invalid_file_format_when_format_version_given_2(self): - self.test_new_data['file_characteristics']['file_format'] = 'nonexisting' - self.test_new_data['file_characteristics']['format_version'] = 'any' - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_characteristics' in response.data.keys(), True) - self.assertEqual('file_characteristics.file_format' in response.data['file_characteristics'], True) - - def test_file_format_version_with_invalid_format_version_when_file_format_has_versions_1(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_with_version['input_file_format'] - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_characteristics' in response.data.keys(), True) - self.assertEqual('file_characteristics.format_version' in response.data['file_characteristics'], True) - - def test_file_format_version_with_invalid_format_version_when_file_format_has_versions_2(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_with_version['input_file_format'] - self.test_new_data['file_characteristics']['format_version'] = 'nonexisting' - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_characteristics' in response.data.keys(), True) - self.assertEqual('file_characteristics.format_version' in response.data['file_characteristics'], True) - - def test_file_format_version_with_empty_format_version_when_file_format_has_no_version_1(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_without_version['input_file_format'] - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - def test_file_format_version_with_empty_format_version_when_file_format_has_no_version_2(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_without_version['input_file_format'] - self.test_new_data['file_characteristics']['format_version'] = '' - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - def test_file_format_version_with_valid_file_format_and_valid_file_version_1(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_with_version['input_file_format'] - self.test_new_data['file_characteristics']['format_version'] = self.ff_with_version['output_format_version'] - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - def test_file_format_version_with_valid_file_format_and_valid_file_version_2(self): - self.test_new_data['file_characteristics']['file_format'] = self.ff_with_version['input_file_format'] - self.test_new_data['file_characteristics']['format_version'] = \ - self.ff_with_different_version['output_format_version'] - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertTrue(self.ff_with_version["output_format_version"] != "") + self.assertTrue(self.ff_with_different_version["output_format_version"] != "") + self.assertTrue( + self.ff_with_version["input_file_format"] + == self.ff_with_different_version["input_file_format"] + ) + self.assertTrue( + self.ff_with_version["output_format_version"] + != self.ff_with_different_version["output_format_version"] + ) + self.assertTrue(self.ff_without_version["output_format_version"] == "") # update tests - def test_file_characteristics_is_validated_on_update(self): - """ - Ensure validation also works when updating existing files. - """ - self.test_new_data['file_characteristics']['file_format'] = self.ff_without_version['input_file_format'] - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - response = self.client.put('/rest/v2/files/%s' % response.data['identifier'], response.data, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - def test_format_version_is_removed(self): """ Empty file format version should be removed """ - self.test_new_data['file_characteristics']['file_format'] = "text/csv" - self.test_new_data['file_characteristics']['format_version'] = "" - - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertTrue('format_version' not in response.data['file_characteristics']) - -class FileApiWriteCreateTests(FileApiWriteCommon): - # - # - # - # create apis - # - # - # - - def test_create_file(self): - # note: leading and trailing whitespace must be preserved. - newly_created_file_name = " MX .201015_Suomessa_tavattavat_ruokasammakot_ovat_väritykseltään_vaihtelevia_" \ - "osa_on_ruskeita,_osa_kirkkaankin_vihreitä._Vihersammakoiden_silmät_ovat_kohtalaisen_korkealla_päälae" \ - "lla._Sammakkolampi.fi_CC-BY-NC-4.0_thumb.jpg.meta " - self.test_new_data['file_name'] = newly_created_file_name - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' - - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('file_name' in response.data.keys(), True) - self.assertEqual(response.data['file_name'], newly_created_file_name) - self._check_project_root_byte_size_and_file_count(response.data['project_identifier']) - - def test_create_file_error_identifier_exists(self): - # first ok - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - # second should give error - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('identifier' in response.data.keys(), True) - self.assertEqual('already exists' in response.data['identifier'][0], True) - - def test_allow_creating_previously_deleted_file(self): - """ - It should be possible to delete a file, and then create the exact same file again - without letting the removed file conflict. - """ - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - response = self.client.delete('/rest/v2/files/%d' % response.data['id'], format="json") - - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - def test_create_file_error_json_validation(self): - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' - self.test_new_data['file_characteristics'] = { - "application_name": "Application Name", - "description": "A nice description 0000000010", - "metadata_modified": 12345, - "file_created": "2014-01-17T08:19:31Z", - "encoding": "utf-8", - "title": "A title 0000000010" - } - - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_characteristics' in response.data.keys(), True, - 'The error should concern the field file_characteristics') - self.assertEqual('metadata_modified' in response.data['file_characteristics'][0], True, - 'The error should contain the name of the erroneous field') - self.assertEqual('Json path:' in response.data['file_characteristics'][0], True, - 'The error should contain the json path') - - def test_create_file_allowed_checksum_algorithm(self): - self.test_new_data['checksum']['algorithm'] = 'SHA-512' - - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['checksum']['algorithm'], 'SHA-512') - - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-md5' - self.test_new_data['file_path'] = '/md5/filepath/md5-filename' - self.test_new_data['file_name'] = 'md5-filename' - self.test_new_data['checksum']['algorithm'] = 'MD5' - - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['checksum']['algorithm'], 'MD5') - - def test_create_file_not_allowed_checksum_algorithm(self): - from django.db import transaction - - for algo in ['sha2', 'sha256', 'sha-256']: - # run POST requests inside db transaction to ensure django testcase transactions - # work correctly. https://stackoverflow.com/a/23326971/1201945 this probably has - # somethind to do with the fact that POST requests to /rest/v2/files do not normally - # execute inside a db transaction like all other requests to metax api do. see - # file_view.py for details. - # - # alternative for below would be to use optional query param ?dryrun=true, which - # causes the request to be executed inside a transaction too. - with transaction.atomic(): - self.test_new_data['checksum']['algorithm'] = algo - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('checksum_algorithm' in response.data, True) - - # - # create list operations - # - - def test_create_file_list(self): - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' - self._change_file_path(self.test_new_data, 'one_file.txt') - - self.second_test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurnalso' - self._change_file_path(self.second_test_new_data, 'two_file.txt') - - response = self.client.post('/rest/v2/files', [self.test_new_data, self.second_test_new_data], format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('object' in response.data['success'][0].keys(), True) - self.assertEqual(len(response.data['failed']), 0, response.data['failed']) - self.assertEqual(len(response.data['success']), 2) - self._check_project_root_byte_size_and_file_count(response.data['success'][0]['object']['project_identifier']) - - # ensure structure of some specific fields is the same as when single files are created - self.assertEqual('identifier' in response.data['success'][0]['object']['file_storage'], True) - self.assertEqual('identifier' in response.data['success'][0]['object']['parent_directory'], True) - self.assertEqual('checksum' in response.data['success'][0]['object'], True) - self.assertEqual('value' in response.data['success'][0]['object']['checksum'], True) - - def test_create_file_list_error_one_fails(self): - newly_created_file_name = 'newly_created_file_name' - self.test_new_data['file_name'] = newly_created_file_name - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' - # same as above - should fail - self.second_test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' - - response = self.client.post('/rest/v2/files', [self.test_new_data, self.second_test_new_data], format="json") - - """ - List response looks like - { - 'success': [ - { 'object': object }, - more objects... - ], - 'failed': [ - { - 'object': object, - 'errors': {'field': ['message'], 'otherfiled': ['message']} - }, - more objects... - ] - } - """ - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('object' in response.data['failed'][0].keys(), True) - self.assertEqual('file_name' in response.data['failed'][0]['object'].keys(), True) - self.assertEqual('identifier' in response.data['failed'][0]['errors'], True, - 'The error should have been about an already existing identifier') - - def test_parameter_ignore_already_exists_errors(self): - newly_created_file_name = 'newly_created_file_name' - self.test_new_data['file_name'] = newly_created_file_name - self.test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' - # same as above - should cause an error. - self.second_test_new_data['identifier'] = 'urn:nbn:fi:csc-thisisanewurn' - - response = self.client.post('/rest/v2/files?ignore_already_exists_errors', - [self.test_new_data, self.second_test_new_data], format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(len(response.data), 2) - self.assertEqual('already exists' in response.data['success'][1]['object']['detail'], True) - - def test_create_file_list_error_all_fail(self): - newly_created_file_name = 'newly_created_file_name' - self.test_new_data['file_name'] = newly_created_file_name - # identifier is a required field, should fail - self.test_new_data['identifier'] = None - self.second_test_new_data['identifier'] = None - - response = self.client.post('/rest/v2/files', [self.test_new_data, self.second_test_new_data], format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual('failed' in response.data.keys(), True) - self.assertEqual('object' in response.data['failed'][0].keys(), True) - self.assertEqual(len(response.data['success']), 0) - self.assertEqual(len(response.data['failed']), 2) - - -class FileApiWriteCreateDirectoriesTests(FileApiWriteCommon): - - """ - Only checking directories related stuff in these tests - """ - - def test_create_file_hierarchy_from_single_file(self): - """ - Create, from a single file, a file hierarchy for a project which has 0 files - or directories created previously. - """ - - f = self._form_complex_list_from_test_file()[0] - file_path = '/project_y_FROZEN/Experiment_1/path/of/lonely/file_and_this_also_has_to_support' \ - 'veryverylooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo' \ - 'ooooooooooooooooooooooooooooooooooooooongdirectorynames/%s' - f['file_path'] = file_path % f['file_name'] - f['identifier'] = 'abc123111' - - response = self.client.post('/rest/v2/files', f, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('date_created' in response.data, True) - self.assertEqual('parent_directory' in response.data, True) - - dirs_count = Directory.objects.filter(project_identifier='project_y').count() - dirs_created_count = self._count_dirs_from_path(f['file_path']) - self.assertEqual(dirs_count, dirs_created_count) - - def test_create_file_append_to_existing_directory(self): - """ - Appending a file to an existing file hierarchy should not cause any other - changes in any other directories. - - Note: Targeting project_x, which exists in pre-generated test data. - """ - project_identifier = 'project_x' - dir_count_before = Directory.objects.filter(project_identifier=project_identifier).count() - file_count_before = Directory.objects.filter(project_identifier=project_identifier, - directory_path='/project_x_FROZEN/Experiment_X/Phase_1').first().files.all().count() - - f = self._form_complex_list_from_test_file()[0] - f['file_path'] = '/project_x_FROZEN/Experiment_X/Phase_1/%s' % f['file_name'] - f['identifier'] = '%s-111' % f['file_path'] - f['project_identifier'] = project_identifier - - response = self.client.post('/rest/v2/files', f, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('date_created' in response.data, True) - self.assertEqual('parent_directory' in response.data, True) - - dir_count_after = Directory.objects.filter(project_identifier=project_identifier).count() - file_count_after = Directory.objects.filter(project_identifier=project_identifier, - directory_path='/project_x_FROZEN/Experiment_X/Phase_1').first().files.all().count() - self.assertEqual(dir_count_before, dir_count_after) - self.assertEqual(file_count_after - file_count_before, 1) - - def test_create_file_hierarchy_from_file_list_with_no_existing_files(self): - """ - Create a file hierarchy for a project which has 0 files or directories created previously. - - Here, a directory /project_y_FROZEN/Experiment_1 is "frozen" - """ - experiment_1_file_list = self._form_complex_list_from_test_file() - - response = self.client.post('/rest/v2/files', experiment_1_file_list, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 12) - self.assertEqual(len(response.data['failed']), 0) - - dirs_dict = self._assert_directory_parent_dirs('project_y') - self._assert_file_parent_dirs(dirs_dict, response) - - def test_create_file_hierarchy_from_file_list_with_existing_files(self): - """ - Create a file hierarchy for a project which already has files or directories - created previously. - - Here the interesting part is, the top-most dir in the file list should find - an existing directory, which it can use as its parent dir. - - Here, a directory /project_y_FROZEN/Experiment_2/Phase_1/Data is "frozen", - when /project_y_FROZEN already exists. - """ - - # setup db to have pre-existing dirs - experiment_1_file_list = self._form_complex_list_from_test_file() - response = self.client.post('/rest/v2/files', experiment_1_file_list, format="json") - - # form new test data - experiment_2_file_list = self._form_complex_list_from_test_file() - - for i, f in enumerate(experiment_2_file_list): - f['file_path'] = f['file_path'].replace('/project_y_FROZEN/Experiment_1', - '/project_y_FROZEN/Experiment_2/Phase_1/Data') - f['identifier'] = '%s-%d' % (f['file_path'], i) - - response = self.client.post('/rest/v2/files', experiment_2_file_list, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 12) - self.assertEqual(len(response.data['failed']), 0) - - dirs_dict = self._assert_directory_parent_dirs('project_y') - self._assert_file_parent_dirs(dirs_dict, response) - - def test_append_files_to_existing_directory(self): - """ - Append some files to an existing directory. - - Here, 5 files are added to directory /project_y_FROZEN/Experiment_2/ - """ - - # setup db to have pre-existing dirs - experiment_1_file_list = self._form_complex_list_from_test_file() - response = self.client.post('/rest/v2/files', experiment_1_file_list, format="json") - - # form new test data, and trim it down a bit - experiment_2_file_list = self._form_complex_list_from_test_file() - while len(experiment_2_file_list) > 5: - experiment_2_file_list.pop() - - for i, f in enumerate(experiment_2_file_list): - f['file_path'] = '/project_y_FROZEN/Experiment_2/%s' % f['file_name'] - f['identifier'] = '%s-%d' % (f['file_path'], i) - - response = self.client.post('/rest/v2/files', experiment_2_file_list, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual(len(response.data['failed']), 0, response.data['failed']) - self.assertEqual(len(response.data['success']), 5) - - dirs_dict = self._assert_directory_parent_dirs('project_y') - self._assert_file_parent_dirs(dirs_dict, response) - - def test_append_one_file_to_existing_directory(self): - """ - Append one file to an existing directory. - - Here, 1 file is added to directory /project_y_FROZEN/Experiment_2/ - """ - - # setup db to have pre-existing dirs - experiment_1_file_list = self._form_complex_list_from_test_file() - response = self.client.post('/rest/v2/files', experiment_1_file_list, format="json") - - # form new test data, but use just the first item - experiment_2_file_list = self._form_complex_list_from_test_file()[0:1] - - for i, f in enumerate(experiment_2_file_list): - f['file_path'] = '/project_y_FROZEN/Experiment_2/%s' % f['file_name'] - f['identifier'] = '%s-%d' % (f['file_path'], i) - - response = self.client.post('/rest/v2/files', experiment_2_file_list, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual('success' in response.data.keys(), True) - self.assertEqual(len(response.data['success']), 1) - self.assertEqual(len(response.data['failed']), 0) - - dirs_dict = self._assert_directory_parent_dirs('project_y') - self._assert_file_parent_dirs(dirs_dict, response) - - def test_create_file_hierarchy_error_file_list_has_invalid_data(self): - """ - If even one file is missing file_path or project_identifier, the - request is immediately terminated. Creating files for multiple projects - in a single request is also not permitted. - """ - experiment_1_file_list = self._form_complex_list_from_test_file() - experiment_1_file_list[0].pop('file_path') - response = self.client.post('/rest/v2/files', experiment_1_file_list, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('file_path' in response.data, True) - self.assertEqual('required parameter' in response.data['file_path'][0], True) - - experiment_1_file_list = self._form_complex_list_from_test_file() - experiment_1_file_list[0].pop('project_identifier') - response = self.client.post('/rest/v2/files', experiment_1_file_list, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('project_identifier' in response.data, True) - self.assertEqual('required parameter' in response.data['project_identifier'][0], True) - - experiment_1_file_list = self._form_complex_list_from_test_file() - experiment_1_file_list[0]['project_identifier'] = 'second_project' - response = self.client.post('/rest/v2/files', experiment_1_file_list, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('project_identifier' in response.data, True) - self.assertEqual('multiple projects' in response.data['project_identifier'][0], True) - - def test_filepath_starts_with_slash(self): - file = self._get_new_test_data() - file['file_path'] = file['file_path'][1:] - - response = self.client.post('/rest/v2/files', file, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue("file path should start with '/' to point to the root" in response.data['file_path'][0]) - - def _assert_directory_parent_dirs(self, project_identifier): - """ - Check dirs created during the request have parent dirs as expected. - """ - dirs_dict = {} - - for d in Directory.objects.filter(project_identifier=project_identifier): - dirs_dict[d.directory_path] = { - 'dir_id': d.id, - 'parent_dir_id': d.parent_directory and d.parent_directory.id or None - } - - for dir_path, ids in dirs_dict.items(): - if dir_path == '/': - self.assertEqual(ids['parent_dir_id'], None, 'root dir \'/\' should not have a parent directory') - continue - expected_parent_dir_path = dirname(dir_path) - self.assertEqual(ids['parent_dir_id'], dirs_dict[expected_parent_dir_path]['dir_id'], - 'parent dir not as expected.') - - return dirs_dict - - def _assert_file_parent_dirs(self, dirs_dict, response): - """ - Check files have parent dirs as expected. - """ - for entry in response.data['success']: - f = entry['object'] - excpected_parent_dir_path = dirname(f['file_path']) - self.assertEqual(f['parent_directory']['id'], dirs_dict[excpected_parent_dir_path]['dir_id'], - 'parent dir not as expected.') - - def _form_complex_list_from_test_file(self): - """ - "complex" list. Notice the leading and trailing whitespace in directories Group_1 and Group_3. - """ - dir_data = [ - { - "file_name": "uudehdko.png", - "file_path": "/project_y_FROZEN/Experiment_1/Group_1 /Results/uudehdko.png", - }, - { - "file_name": "uusi.png", - "file_path": "/project_y_FROZEN/Experiment_1/Group_1 /Results/uusi.png", - }, - { - "file_name": "path.png", - "file_path": "/project_y_FROZEN/Experiment_1/Group_1 /path.png", - }, - { - "file_name": "b_path.png", - "file_path": "/project_y_FROZEN/Experiment_1/b_path.png", - }, - { - "file_name": "everything_that_can_go_wrong_will_go_wrong.png", - "file_path": "/project_y_FROZEN/Experiment_1/Group_2/everything_that_can_go_wrong_will_go_wrong.png", - }, - { - "file_name": "pathx.png", - "file_path": "/project_y_FROZEN/Experiment_1/pathx.png", - }, - { - "file_name": "kansio.png", - "file_path": "/project_y_FROZEN/Experiment_1/Group_1 /Results/Important/kansio.png", - }, - { - "file_name": "some.png", - "file_path": "/project_y_FROZEN/Experiment_1/some.png", - }, - { - "file_name": "aa_toka.png", - "file_path": "/project_y_FROZEN/Experiment_1/aa_toka.png", - }, - { - "file_name": "aa_eka.png", - "file_path": "/project_y_FROZEN/Experiment_1/aa_eka.png", - }, - { - "file_name": "kissa.png", - "file_path": "/project_y_FROZEN/Experiment_1/ Group_3/2017/01/kissa.png", - }, - { - "file_name": "ekaa.png", - "file_path": "/project_y_FROZEN/Experiment_1/ekaa.png", - }, - ] - - template = self.test_new_data - template.pop('id', None) - template.pop('identifier', None) - template.pop('project_identifier', None) - template.pop('parent_directory', None) - template.pop('date_created', None) - template.pop('date_modified', None) - template.pop('service_created', None) - - files = [] - for i, d in enumerate(dir_data): - files.append(deepcopy(template)) - files[-1].update(d, identifier='pid:urn:test:file:%d' % i, project_identifier='project_y') - - return files - - -class FileApiWriteUpdateTests(FileApiWriteCommon): - """ - update operations PUT - """ - - def test_update_file(self): - f = self.client.get('/rest/v2/files/1').data - f['file_format'] = 'csv' - response = self.client.put('/rest/v2/files/%s' % f['identifier'], f, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_prevent_file_path_update_after_create(self): - f = self.client.get('/rest/v2/files/1').data - f['file_path'] = '%s_bak' % f['file_path'] - response = self.client.put('/rest/v2/files/%s' % f['identifier'], f, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_update_file_error_required_fields(self): - """ - Field 'project_identifier' is missing, which should result in an error, since PUT - replaces an object and requires all 'required' fields to be present. - """ - self.test_new_data.pop('project_identifier') - response = self.client.put('/rest/v2/files/%s' % self.identifier, self.test_new_data, format="json") + self.test_new_data["file_characteristics"]["file_format"] = "text/csv" + self.test_new_data["file_characteristics"]["format_version"] = "" - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('project_identifier' in response.data.keys(), True, - 'Error for field \'project_identifier\' is missing from response.data') - - def test_update_file_not_found(self): - response = self.client.put('/rest/v2/files/doesnotexist', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_update_file_allowed_projects_ok(self): - f = self.client.get('/rest/v2/files/1').data - response = self.client.put('/rest/v2/files/%s?allowed_projects=%s' % (f['identifier'], f['project_identifier']), - f, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_update_file_allowed_projects_fail(self): - f = self.client.get('/rest/v2/files/1').data - response = self.client.put( - '/rest/v2/files/%s?allowed_projects=nopermission' % f['identifier'], f, format="json" - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_update_file_allowed_projects_not_dict(self): - f = self.client.get('/rest/v2/files/1').data - response = self.client.put('/rest/v2/files/%s?allowed_projects=%s' % (f['identifier'], f['project_identifier']), - [f], format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('json' in response.data['detail'][0], True, 'Error regarding datatype') - - # - # update list operations PUT - # - - def test_file_update_list(self): - f1 = self.client.get('/rest/v2/files/1').data - f2 = self.client.get('/rest/v2/files/2').data - new_file_format = 'changed-format' - new_file_format_2 = 'changed-format-2' - f1['file_format'] = new_file_format - f2['file_format'] = new_file_format_2 - - response = self.client.put('/rest/v2/files', [f1, f2], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - updated_file = File.objects.get(pk=1) - self.assertEqual(updated_file.file_format, new_file_format) - - def test_file_update_list_error_one_fails(self): - f1 = self.client.get('/rest/v2/files/1').data - f2 = self.client.get('/rest/v2/files/2').data - new_file_format = 'changed-format' - f1['file_format'] = new_file_format - # cant be null - should fail - f2['file_frozen'] = None - - response = self.client.put('/rest/v2/files', [f1, f2], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['success']), 1, 'success list should be empty') - self.assertEqual(len(response.data['failed']), 1, 'there should have been one failed element') - self.assertEqual('file_frozen' in response.data['failed'][0]['errors'], True, - 'error should be about file_characteristics missing') - - updated_file = File.objects.get(pk=1) - self.assertEqual(updated_file.file_format, new_file_format) - - def test_file_update_list_error_key_not_found(self): - f1 = self.client.get('/rest/v2/files/1').data - f2 = self.client.get('/rest/v2/files/2').data - new_file_format = 'changed-format' - new_file_format_2 = 'changed-format-2' - f1['file_format'] = new_file_format - f2['file_format'] = new_file_format_2 - # has no lookup key - should fail - f2.pop('id') - f2.pop('identifier') - - response = self.client.put('/rest/v2/files', [f1, f2], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['success']), 1, 'success list should be empty') - self.assertEqual(len(response.data['failed']), 1, 'there should have been one failed element') - error_msg_of_failed_row = response.data['failed'][0]['errors']['detail'][0] - self.assertEqual('identifying keys' in error_msg_of_failed_row, True, - 'error should be about identifying keys missing') - - updated_file = File.objects.get(pk=1) - self.assertEqual(updated_file.file_format, new_file_format) - - def test_file_update_list_allowed_projects_ok(self): - # Both files in project 'project_x' - f1 = self.client.get('/rest/v2/files/1').data - f2 = self.client.get('/rest/v2/files/2').data - - response = self.client.put('/rest/v2/files?allowed_projects=project_x,y,z', [f1, f2], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_file_update_list_allowed_projects_fail(self): - # Files in projects 'project_x' and 'research_project_112' - f1 = self.client.get('/rest/v2/files/1').data - f2 = self.client.get('/rest/v2/files/39').data - - response = self.client.put('/rest/v2/files?allowed_projects=project_x,y,z', [f1, f2], format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_file_update_list_allowed_projects_empty_value(self): - f1 = self.client.get('/rest/v2/files/1').data - response = self.client.put('/rest/v2/files?allowed_projects=', [f1], format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_file_update_list_allowed_projects_not_list(self): - new_data_1 = {} - new_data_1['identifier'] = "pid:urn:1" - new_data_1['file_name'] = 'Nice_new_name' - - res = self.client.patch('/rest/v2/files?allowed_projects=y,z,project_x', new_data_1, format="json") - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST, res.data) - - -class FileApiWritePartialUpdateTests(FileApiWriteCommon): - """ - update operations PATCH - """ - - def test_update_file_partial(self): - new_data = { - "file_name": "new_file_name", - } - response = self.client.patch('/rest/v2/files/%s' % self.identifier, new_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('file_name' in response.data.keys(), True) - self.assertEqual('file_path' in response.data.keys(), True, 'PATCH operation should return full content') - self.assertEqual(response.data['file_name'], 'new_file_name', 'Field file_name was not updated') - - def test_update_partial_allowed_projects_ok(self): - new_data = { - "file_name": "new_file_name", - } - response = self.client.patch('/rest/v2/files/%s?allowed_projects=%s' % (self.identifier, self.pidentifier), - new_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['file_name'], 'new_file_name', response.data) - - def test_update_partial_allowed_projects_fail(self): - new_data = { - "file_name": "new_file_name", - } - response = self.client.patch('/rest/v2/files/%s?allowed_projects=noproject' % self.identifier, - new_data, format="json") - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_update_partial_allowed_projects_not_dict(self): - new_data = { - "file_name": "new_file_name", - } - response = self.client.patch('/rest/v2/files/%s?allowed_projects=%s' % (self.identifier, self.pidentifier), - [new_data], format="json") - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('json' in response.data['detail'][0], True, 'Error regarding datatype') - - # - # update list operations PATCH - # - - def test_file_partial_update_list(self): - new_project_identifier = 'changed-project-identifier' - new_project_identifier_2 = 'changed-project-identifier-2' - - test_data = {} - test_data['id'] = 1 - test_data['project_identifier'] = new_project_identifier - - second_test_data = {} - second_test_data['id'] = 2 - second_test_data['project_identifier'] = new_project_identifier_2 - - response = self.client.patch('/rest/v2/files', [test_data, second_test_data], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual('success' in response.data, True, 'response.data should contain list of changed objects') - self.assertEqual(len(response.data['success']), 2, 'response.data should contain 2 changed objects') - self.assertEqual('file_characteristics' in response.data['success'][0]['object'], True, - 'response.data should contain full objects') - - updated_file = File.objects.get(pk=1) - self.assertEqual(updated_file.project_identifier, new_project_identifier, 'project_identifier did not update') - - def test_file_partial_update_list_allowed_projects_ok(self): - new_data_1 = {} - new_data_1['identifier'] = "pid:urn:1" - new_data_1['file_name'] = 'Nice_new_name' - - new_data_2 = {} - new_data_2['identifier'] = "pid:urn:2" - new_data_2['file_name'] = 'Not_so_nice_name' - - res = self.client.patch( - '/rest/v2/files?allowed_projects=y,z,project_x', [new_data_1, new_data_2], format="json" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK, res.data) - self.assertEqual(res.data['success'][0]['object']['file_name'], 'Nice_new_name', res.data) - - def test_file_partial_update_list_allowed_projects_fail(self): - # Files in projects 'project_x' and 'research_project_112' - f1 = self.client.get('/rest/v2/files/1').data - f2 = self.client.get('/rest/v2/files/39').data - - response = self.client.patch('/rest/v2/files?allowed_projects=project_x,y,z', [f1, f2], format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) - - def test_file_partial_update_list_allowed_projects_not_list(self): - new_data_1 = {} - new_data_1['identifier'] = "pid:urn:1" - new_data_1['file_name'] = 'Nice_new_name' - - res = self.client.patch('/rest/v2/files?allowed_projects=y,z,project_x', new_data_1, format="json") - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST, res.data) - - def test_file_partial_update_list_allowed_projects_no_identifier(self): - new_data_1 = {} - new_data_1['file_name'] = 'Nice_new_name' - - new_data_2 = {} - new_data_2['id'] = 23 - new_data_2['file_name'] = 'Not_so_nice_name' - - res = self.client.patch( - '/rest/v2/files?allowed_projects=y,z,project_x', [new_data_1, new_data_2], format="json" - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST, res.data) - - -class FileApiWriteDeleteTests(FileApiWriteCommon): - # - # - # - # delete apis - # - # - # - - def test_delete_single_file_ok(self): - dir_count_before = Directory.objects.all().count() - response = self.client.delete('/rest/v2/files/1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('deleted_files_count' in response.data, True, response.data) - self.assertEqual(response.data['deleted_files_count'], 1, response.data) - dir_count_after = Directory.objects.all().count() - self.assertEqual(dir_count_before, dir_count_after, 'no dirs should have been deleted') - deleted_file = File.objects_unfiltered.get(pk=1) - self._check_project_root_byte_size_and_file_count(deleted_file.project_identifier) - self.assertEqual(deleted_file.date_modified, deleted_file.file_deleted, 'date_modified should be updated') - - def test_delete_single_file_ok_destroy_leading_dirs(self): - project_identifier = 'project_z' - test_data = deepcopy(self.test_new_data) - test_data['file_path'] = '/project_z/some/path/here/%s' % test_data['file_name'] - test_data['project_identifier'] = project_identifier - test_data['identifier'] = 'abc123' - response = self.client.post('/rest/v2/files', test_data, format='json') - self.assertEqual(Directory.objects.filter(project_identifier=project_identifier).exists(), True) - - response = self.client.delete('/rest/v2/files/%s' % response.data['id']) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('deleted_files_count' in response.data, True, response.data) - self.assertEqual(response.data['deleted_files_count'], 1, response.data) - - self.assertEqual(Directory.objects.filter(project_identifier=project_identifier).exists(), False) - - def test_delete_single_file_404(self): - response = self.client.delete('/rest/v2/files/doesnotexist') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_bulk_delete_files_identifiers_not_found(self): - """ - A bulk delete request to /files, but any of the identifiers provided are not found. - Should return 404. - """ - identifiers = ['nope', 'doesnotexist', 'stillno'] - response = self.client.delete('/rest/v2/files', identifiers, format="json") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) - - def test_bulk_delete_files_some_identifiers_not_found(self): - """ - A bulk delete request to /files, but some of the identifiers provided are not found. - Should be ok, delete those files that are found. Assumably those identifiers that were - not found did not exist anyway, therefore no harm is done. - """ - identifiers = ['nope', 'doesnotexist', 'stillno'] - identifiers.append(File.objects.get(pk=1).identifier) - response = self.client.delete('/rest/v2/files', identifiers, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - removed = File.objects_unfiltered.get(pk=1).removed - self.assertEqual(removed, True, 'file should have been removed') - self._check_project_root_byte_size_and_file_count(File.objects_unfiltered.get(pk=1).project_identifier) - - def test_bulk_delete_files_in_single_directory_1(self): - """ - A bulk delete request to /files, where the list of files does not contain a full - directory and all its sub-directories. - - Only the files requested should be deleted, while leaving the rest of the directory - tree intact. - """ - all_files_count_before = File.objects.all().count() - file_ids = [f.id for f in Directory.objects.get(pk=3).files.all()] - - response = self.client.delete('/rest/v2/files', file_ids, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - all_files_count_after = File.objects.all().count() - self.assertEqual(all_files_count_after, all_files_count_before - len(file_ids)) - - def test_bulk_delete_files_in_single_directory_2(self): - """ - Same as above, but target another directory. - """ - all_files_count_before = File.objects.all().count() - file_ids = [f.id for f in Directory.objects.get(pk=4).files.all()] - - response = self.client.delete('/rest/v2/files', file_ids, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - all_files_count_after = File.objects.all().count() - self.assertEqual(all_files_count_after, all_files_count_before - len(file_ids)) - - def test_bulk_delete_file_list_one_file_id_missing(self): - """ - Otherwise complete set of files, but from one dir one file is missing. - Should leave the one file intact, while preserving the directory tree. - """ - all_files_count_before = File.objects.filter(project_identifier='project_x').count() - file_ids = [f.id for f in File.objects.filter(project_identifier='project_x')] - - # everything except the last file should be removed - file_ids.pop() - - response = self.client.delete('/rest/v2/files', file_ids, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - all_files_after = File.objects.filter(project_identifier='project_x') - self.assertEqual(all_files_after.count(), all_files_count_before - len(file_ids)) - - expected_dirs_count = self._count_dirs_from_path(all_files_after[0].file_path) - actual_dirs_count = Directory.objects.filter(project_identifier='project_x').count() - self.assertEqual(actual_dirs_count, expected_dirs_count) - - def test_bulk_delete_files_from_root(self): - """ - Delete all files in project_x. The top-most dir should be /project_x_FROZEN/Experiment_X, - so the whole tree should end up being deleted. - """ - files_to_remove_count = 20 - file_ids = File.objects.filter(project_identifier='project_x').values_list('id', flat=True) - self.assertEqual(len(file_ids), files_to_remove_count) - - response = self.client.delete('/rest/v2/files', file_ids, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('deleted_files_count', None), files_to_remove_count, response.data) - - self._assert_files_available_and_removed('project_x', 0, files_to_remove_count) - self.assertEqual(Directory.objects_unfiltered.filter(project_identifier='project_x').count(), 0, - 'all dirs should have been permanently removed') - - def test_bulk_delete_sub_directory_1(self): - """ - Delete from /project_x_FROZEN/Experiment_X/Phase_1, which should remove - only 15 files. - """ - files_to_remove_count = 15 - file_ids = [f.id for f in Directory.objects.get(pk=4).files.all()] - file_ids += [f.id for f in Directory.objects.get(pk=6).files.all()] - self.assertEqual(len(file_ids), files_to_remove_count) - - response = self.client.delete('/rest/v2/files', file_ids, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('deleted_files_count', None), files_to_remove_count, response.data) - - self._assert_files_available_and_removed('project_x', 5, files_to_remove_count) - - # these dirs should still be left: - # / - # /project_x_FROZEN - # /project_x_FROZEN/Experiment_X (has 5 files) - self.assertEqual(Directory.objects.filter(project_identifier='project_x').count(), 3) - - def test_bulk_delete_sub_directory_2(self): - """ - Delete from /project_x_FROZEN/Experiment_X/Phase_1/2017/01, which should - remove only 10 files. - """ - files_to_remove_count = 10 - file_ids = [f.id for f in Directory.objects.get(pk=6).files.all()] - self.assertEqual(len(file_ids), files_to_remove_count) - - response = self.client.delete('/rest/v2/files', file_ids, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('deleted_files_count', None), files_to_remove_count, response.data) - - self._assert_files_available_and_removed('project_x', 10, files_to_remove_count) - - # these dirs should still be left: - # / - # /project_x_FROZEN - # /project_x_FROZEN/Experiment_X (5 files) - # /project_x_FROZEN/Experiment_X/Phase_1 (5 files) - self.assertEqual(Directory.objects.filter(project_identifier='project_x').count(), 4) - - # /project_x_FROZEN/Experiment_X/Phase_1/2017 <- this dir should be deleted, since - # it only contained the 01-dir, which we specifically targeted for deletion - self.assertEqual(Directory.objects.filter( - project_identifier='project_x', - directory_path='/project_x_FROZEN/Experiment_X/Phase_1/2017' - ).count(), 0, 'dir should have been deleted') - - def _assert_files_available_and_removed(self, project_identifier, available, removed): - """ - After deleting files, check qty of files retrievable by usual means is as expected, - and qty of files retrievable from objects_unfiltered with removed=True is as expected. - """ - self.assertEqual(File.objects.filter(project_identifier=project_identifier).count(), available, - 'files should not be retrievable from removed=False scope') - self.assertEqual(File.objects_unfiltered.filter(project_identifier=project_identifier, removed=True).count(), - removed, - 'files should be retrievable from removed=True scope') - - def test_deleting_files_deprecates_datasets(self): - for cr in CatalogRecord.objects.filter(deprecated=True): - # ensure later assert is correct - cr.deprecated = False - cr.force_save() - - datasets_with_file = CatalogRecord.objects.filter(files__id=1).count() - response = self.client.delete('/rest/v2/files/1') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(CatalogRecord.objects.filter(deprecated=True).count(), datasets_with_file) - - -class FileApiWriteRestoreTests(FileApiWriteCommon): - - def test_restore_files_ok(self): - """ - Restore a few deleted files from directories, that still contain other files. - Restored files should be appended to previously existing files. - """ - response = self.client.delete('/rest/v2/files/1') - response = self.client.delete('/rest/v2/files/2') - response = self.client.delete('/rest/v2/files/3') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - deleted_files = File.objects_unfiltered.filter(pk__in=[1, 2, 3]) \ - .values('identifier', 'parent_directory_id') - - response = self.client.post('/rest/v2/files/restore', [f['identifier'] for f in deleted_files], format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('restored_files_count' in response.data, True, response.data) - self.assertEqual(response.data['restored_files_count'], 3, response.data) - - # ensure restored files are using previously existing directories - old_parent_dirs = { f['parent_directory_id'] for f in deleted_files } - files = File.objects.filter(pk__in=[1, 2, 3]) - for f in files: - self.assertEqual(f.file_deleted, None) - self.assertEqual(f.user_modified, None) - self.assertEqual(f.parent_directory_id in old_parent_dirs, True) - - def test_restore_files_recreate_missing_directories(self): - """ - Restore an entire project. Files should have new directories. - """ - proj = File.objects.get(pk=1).project_identifier - - response = self.client.get('/rest/files?project_identifier=%s&fields=identifier&pagination=false' - % proj, format='json') - file_identifiers = [ f['identifier'] for f in response.data ] - - self.client.delete('/rest/v2/files', file_identifiers, format='json') - - deleted_directory_ids = File.objects_unfiltered.filter(identifier__in=file_identifiers) \ - .values_list('parent_directory_id', flat=True) - old_parent_dirs = { id for id in deleted_directory_ids } - - response = self.client.post('/rest/v2/files/restore', file_identifiers, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual('restored_files_count' in response.data, True, response.data) - self.assertEqual(response.data['restored_files_count'], len(file_identifiers), response.data) - - # ensure restored files are using new directories - files = File.objects.filter(identifier__in=file_identifiers) - for f in files: - self.assertEqual(f.parent_directory_id in old_parent_dirs, False) - - def test_check_parameter_is_string_list(self): - response = self.client.post('/rest/v2/files/restore', ['a', 'b', 1], format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_check_files_belong_to_one_project(self): - f1 = File.objects_unfiltered.get(pk=1) - f2 = File.objects_unfiltered.filter().exclude(project_identifier=f1.project_identifier).first() - response = self.client.delete('/rest/v2/files/%d' % f1.id) - response = self.client.delete('/rest/v2/files/%d' % f2.id) - response = self.client.post('/rest/v2/files/restore', [ f1.identifier, f2.identifier ], format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - -class FileApiWriteXmlTests(FileApiWriteCommon): - """ - /files/pid/xml related tests - """ - - def test_xml_api(self): - content_type = 'application/xml' - data = 'tauhketta yeah' - - # create - response = self.client.post('/rest/v2/files/1/xml?namespace=breh', data, content_type=content_type) - self.assertEqual(response.status_code in (200, 201, 204), True) - self.assertEqual('updated stuff' - response = self.client.put('/rest/v2/files/1/xml?namespace=breh', data, content_type=content_type) - self.assertEqual(response.status_code in (200, 201, 204), True) - - # get updated again - response = self.client.get('/rest/v2/files/1/xml?namespace=breh', content_type=content_type, ) - self.assertEqual('updated stuff' in response.data, True) - - # delete - response = self.client.delete('/rest/v2/files/1/xml?namespace=breh', data, content_type=content_type) - self.assertEqual(response.status_code in (200, 201, 204), True) - - response = self.client.delete('/rest/v2/files/1/xml?namespace=bruh', data, content_type=content_type) - self.assertEqual(response.status_code in (200, 201, 204), True) - - # get list - response = self.client.get('/rest/v2/files/1/xml', content_type=content_type) - self.assertEqual(response.status_code in (200, 201, 204), True) - - -class FileApiWriteEndUserAccess(FileApiWriteCommon): - - def setUp(self): - super().setUp() - self.token = get_test_oidc_token() - self._mock_token_validation_succeeds() - - @responses.activate - def test_user_cant_create_files(self): - ''' - Ensure users are unable to create new files. - ''' - - # ensure user belongs to same project - self.token['group_names'].append('IDA01:%s' % self.test_new_data['project_identifier']) - self._use_http_authorization(method='bearer', token=self.token) - - response = self.client.post('/rest/v2/files', self.test_new_data, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - @responses.activate - def test_user_can_only_update_permitted_file_fields(self): - ''' - Ensure users are only able to modify permitted fields. - ''' - # ensure user belongs to same project - proj = File.objects.get(pk=1).project_identifier - self.token['group_names'].append('IDA01:%s' % proj) - self._use_http_authorization(method='bearer', token=self.token) - - response = self.client.get('/rest/v2/files/1', format="json") - file = response.data - original_file = deepcopy(file) - file['byte_size'] = 200 - file['checksum']['value'] = 'changed' - file['parent_directory'] = 1 - file['file_frozen'] = '3' + file['file_frozen'][1:] - file['file_format'] = 'changed' - file['file_name'] = 'changed' - file['file_path'] = '/oh/no' - file['file_storage'] = 2 - file['file_uploaded'] = '3' + file['file_uploaded'][1:] - file['identifier'] = 'changed' - file['open_access'] = True - file['project_identifier'] = 'changed' - file['service_modified'] = 'changed' - file['service_created'] = 'changed' - file['removed'] = True - - # the only field that should be changed - file['file_characteristics'] = { 'title': 'new title'} - - response = self.client.put('/rest/v2/files/1', file, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['file_characteristics']['title'], 'new title', response.data) - - for key, value in response.data.items(): - try: - if key in ('date_modified', 'file_modified'): - # these fields are changed by metax - continue - elif key == 'file_characteristics': - # the field that should have been changed by the user - self.assertNotEqual(original_file[key], response.data[key]) - else: - # must not have changed - self.assertEqual(original_file[key], response.data[key]) - except KeyError as e: - if e.args[0] == 'user_modified': - # added by metax - continue - raise - - @responses.activate - def test_user_can_update_files_in_their_projects(self): - ''' - Ensure users can edit files in projects they are a member of. - ''' - proj = File.objects.only('project_identifier').get(pk=1).project_identifier - - response = self.client.get('/rest/v2/files?project_identifier=%s' % proj, - format="json") - - file = response.data['results'][0] - - self.token['group_names'].append('IDA01:%s' % proj) - self._use_http_authorization(method='bearer', token=self.token) - - response = self.client.put('/rest/v2/files/%s' % file['id'], file, format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - response = self.client.put('/rest/v2/files', [file], format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - @responses.activate - def test_user_cant_update_files_in_others_projects(self): - ''' - Ensure users can not edit files in projects they are not a member of. - ''' - proj = File.objects.only('project_identifier').get(pk=1).project_identifier - - response = self.client.get('/rest/v2/files?project_identifier=%s' % proj, - format="json") - - file = response.data['results'][0] - - self.token['group_names'] = ['no_files_for_this_project'] - self._use_http_authorization(method='bearer', token=self.token) - - response = self.client.put('/rest/v2/files/%s' % file['id'], file, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - response = self.client.put('/rest/v2/files', [file], format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - -class FileApiWriteDryrunTest(FileApiWriteCommon): - - """ - Test query param ?dryrun=bool separately for post /rest/v2/files api, due to special - behavior in POST /rest/v2/files. - - For other apis, the common test case is among views/common/write tests. - """ - - def test_dryrun(self): - """ - Ensure query parameter ?dryrun=true returns same result as they normally would, but - changes made during the request do not get saved in the db. - """ - response = self.client.post('/rest/v2/files?what&dryrun=true&other', self.test_new_data, format="json") + response = self.client.post("/rest/v2/files", self.test_new_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual('id' in response.data, True) - found = File.objects.filter(pk=response.data['id']).exists() - self.assertEqual(found, False, 'file should not get truly created when using parameter dryrun') + self.assertTrue("format_version" not in response.data["file_characteristics"]) \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/filestorages/__init__.py b/src/metax_api/tests/api/rest/v2/views/filestorages/__init__.py deleted file mode 100755 index 6adb6e69..00000000 --- a/src/metax_api/tests/api/rest/v2/views/filestorages/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from .read import * -from .write import * diff --git a/src/metax_api/tests/api/rest/v2/views/filestorages/read.py b/src/metax_api/tests/api/rest/v2/views/filestorages/read.py deleted file mode 100755 index 0a57c688..00000000 --- a/src/metax_api/tests/api/rest/v2/views/filestorages/read.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from django.core.management import call_command -from rest_framework import status -from rest_framework.test import APITestCase - -from metax_api.models import FileStorage -from metax_api.tests.utils import TestClassUtils, test_data_file_path - - -class FileStorageApiReadBasicTests(APITestCase, TestClassUtils): - - @classmethod - def setUpClass(cls): - """ - Loaded only once for test cases inside this class. - """ - call_command('loaddata', test_data_file_path, verbosity=0) - super(FileStorageApiReadBasicTests, cls).setUpClass() - - def setUp(self): - self._use_http_authorization() - - def test_basic_get(self): - fs = FileStorage.objects.get(pk=1) - response = self.client.get('/rest/v2/filestorages/%d' % fs.id) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - response = self.client.get('/rest/v2/filestorages/%s' % fs.file_storage_json['identifier']) - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - def test_basic_list(self): - response = self.client.get('/rest/v2/filestorages') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(len(response.data['results']), FileStorage.objects.all().count(), response.data) diff --git a/src/metax_api/tests/api/rest/v2/views/filestorages/write.py b/src/metax_api/tests/api/rest/v2/views/filestorages/write.py deleted file mode 100755 index 53b5adbb..00000000 --- a/src/metax_api/tests/api/rest/v2/views/filestorages/write.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from django.core.management import call_command -from rest_framework import status -from rest_framework.test import APITestCase - -from metax_api.models import FileStorage -from metax_api.tests.utils import TestClassUtils, test_data_file_path - - -class FileStorageApiWriteCommon(APITestCase, TestClassUtils): - - @classmethod - def setUpClass(cls): - """ - Loaded only once for test cases inside this class. - """ - call_command('loaddata', test_data_file_path, verbosity=0) - super(FileStorageApiWriteCommon, cls).setUpClass() - - def setUp(self): - self.new_test_data = self._get_object_from_test_data('filestorage') - self.new_test_data.pop('id') - self.new_test_data['file_storage_json']['identifier'] = 'new-file-storage' - self._use_http_authorization() - - -class FileStorageApiWriteBasicTests(FileStorageApiWriteCommon): - - def test_create(self): - response = self.client.post('/rest/v2/filestorages', self.new_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - def test_create_identifier_already_exists(self): - response = self.client.post('/rest/v2/filestorages', self.new_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.post('/rest/v2/filestorages', self.new_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('already exists' in response.data['file_storage_json']['identifier'][0], - True, response.data) - - def test_delete(self): - response = self.client.delete('/rest/v2/filestorages/1') - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - fs = FileStorage.objects_unfiltered.get(pk=1) - self.assertEqual(fs.removed, True, 'should be deleted') - self.assertEqual(fs.date_removed, fs.date_modified) diff --git a/src/metax_api/tests/api/rest/v2/views/schemas/__init__.py b/src/metax_api/tests/api/rest/v2/views/schemas/__init__.py deleted file mode 100755 index 0ffba528..00000000 --- a/src/metax_api/tests/api/rest/v2/views/schemas/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from .read import * diff --git a/src/metax_api/tests/api/rest/v2/views/schemas/read.py b/src/metax_api/tests/api/rest/v2/views/schemas/read.py deleted file mode 100755 index 9e210a74..00000000 --- a/src/metax_api/tests/api/rest/v2/views/schemas/read.py +++ /dev/null @@ -1,36 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from rest_framework import status -from rest_framework.test import APITestCase - -from metax_api.tests.utils import TestClassUtils - - -class SchemaApiReadTests(APITestCase, TestClassUtils): - - def test_read_schemas_list(self): - response = self.client.get('/rest/v2/schemas') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['count'] > 0) - - def test_read_schemas_list_html(self): - headers = {'HTTP_ACCEPT': 'text/html'} - response = self.client.get('/rest/v2/schemas', **headers) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response._headers['content-type'][1].find('text/html') >= 0) - - def test_read_schema_retrieve_existing(self): - list_response = self.client.get('/rest/v2/schemas') - self.assertEqual(list_response.status_code, status.HTTP_200_OK) - self.assertTrue(list_response.data['count'] > 0, 'No schemas available') - response = self.client.get('/rest/v2/schemas/%s' % list_response.data['results'][0]) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_read_schema_not_exists(self): - response = self.client.get('/rest/v2/schemas/thisshouldnotexist') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/src/metax_api/tests/api/rpc/base/views/common_rpc.py b/src/metax_api/tests/api/rpc/base/views/common_rpc.py index 1d0c5e7e..abbe3220 100755 --- a/src/metax_api/tests/api/rpc/base/views/common_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/common_rpc.py @@ -12,12 +12,11 @@ class CommonRPCTests(APITestCase, TestClassUtils): - def test_list_valid_methods(self): """ When an invalid (or mistyped) method name is attempted, the api should list valid methods names for that RPC endpoint. """ - response = self.client.get('/rpc/datasets/nonexisting') + response = self.client.get("/rpc/datasets/nonexisting") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('methods are: ' in response.data['detail'][0], True, response.content) + self.assertEqual("methods are: " in response.data["detail"][0], True, response.content) diff --git a/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py b/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py index 84d7bebe..ab033762 100755 --- a/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py @@ -20,14 +20,13 @@ class DatasetRPCTests(APITestCase, TestClassUtils): - @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ super().setUpClass() - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) def setUp(self): super().setUp() @@ -40,81 +39,110 @@ def test_get_minimal_dataset_template(self): """ # query param type is missing, should return error and description what to do. - response = self.client.get('/rpc/datasets/get_minimal_dataset_template') + response = self.client.get("/rpc/datasets/get_minimal_dataset_template") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # test preventing typos - response = self.client.get('/rpc/datasets/get_minimal_dataset_template?type=wrong') + response = self.client.get("/rpc/datasets/get_minimal_dataset_template?type=wrong") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # test minimal dataset for service use - response = self.client.get('/rpc/datasets/get_minimal_dataset_template?type=service') + response = self.client.get("/rpc/datasets/get_minimal_dataset_template?type=service") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue('metadata_provider_org' in response.data) - self.assertTrue('metadata_provider_user' in response.data) - self._use_http_authorization(username='testuser') - response = self.client.post('/rest/datasets', response.data, format="json") + self.assertTrue("metadata_provider_org" in response.data) + self.assertTrue("metadata_provider_user" in response.data) + self._use_http_authorization(username="testuser") + response = self.client.post("/rest/datasets", response.data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) # test minimal dataset for end user use - response = self.client.get('/rpc/datasets/get_minimal_dataset_template?type=enduser') + response = self.client.get("/rpc/datasets/get_minimal_dataset_template?type=enduser") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue("metadata_provider_org" not in response.data) + self.assertTrue("metadata_provider_user" not in response.data) + self._use_http_authorization(method="bearer", token=get_test_oidc_token()) + self._mock_token_validation_succeeds() + response = self.client.post("/rest/datasets", response.data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # test minimal dataset for PAS service use + response = self.client.get("/rpc/datasets/get_minimal_dataset_template?type=service_pas") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue("metadata_provider_org" in response.data) + self.assertTrue("metadata_provider_user" in response.data) + self.assertEqual(response.data["research_dataset"]["issued"], "2019-01-01") + self.assertEqual(response.data["research_dataset"]["publisher"], response.data["research_dataset"]["creator"][0]) + self._use_http_authorization(username="testuser") + response = self.client.post("/rest/datasets", response.data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # test minimal dataset for PAS end user use + response = self.client.get("/rpc/datasets/get_minimal_dataset_template?type=enduser_pas") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue('metadata_provider_org' not in response.data) - self.assertTrue('metadata_provider_user' not in response.data) - self._use_http_authorization(method='bearer', token=get_test_oidc_token()) + self.assertTrue("metadata_provider_org" not in response.data) + self.assertTrue("metadata_provider_user" not in response.data) + self.assertEqual(response.data["research_dataset"]["issued"], "2019-01-01") + self.assertEqual(response.data["research_dataset"]["publisher"], response.data["research_dataset"]["creator"][0]) + self._use_http_authorization(method="bearer", token=get_test_oidc_token()) self._mock_token_validation_succeeds() - response = self.client.post('/rest/datasets', response.data, format="json") + response = self.client.post("/rest/datasets", response.data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_set_preservation_identifier(self): - self._set_http_authorization('service') + self._set_http_authorization("service") # Parameter 'identifier' is required - response = self.client.post('/rpc/datasets/set_preservation_identifier') + response = self.client.post("/rpc/datasets/set_preservation_identifier") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # Nonexisting identifier should return 404 - response = self.client.post('/rpc/datasets/set_preservation_identifier?identifier=nonexisting') + response = self.client.post( + "/rpc/datasets/set_preservation_identifier?identifier=nonexisting" + ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) # Create ida data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) + dc = self._get_object_from_test_data("datacatalog", requested_index=0) dc_id = settings.IDA_DATA_CATALOG_IDENTIFIER - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/datacatalogs', dc, format="json") + dc["catalog_json"]["identifier"] = dc_id + self.client.post("/rest/datacatalogs", dc, format="json") # Test OK ops # Create new ida cr without doi - cr_json = self.client.get('/rest/datasets/1').data - cr_json.pop('preservation_identifier', None) - cr_json.pop('identifier') - cr_json['research_dataset'].pop('preferred_identifier', None) - cr_json['data_catalog'] = dc_id - cr_json['research_dataset']['issued'] = '2018-01-01' - cr_json['research_dataset']['publisher'] = { - '@type': 'Organization', - 'name': { 'en': 'publisher' } + cr_json = self.client.get("/rest/datasets/1").data + cr_json.pop("preservation_identifier", None) + cr_json.pop("identifier") + cr_json["research_dataset"].pop("preferred_identifier", None) + cr_json["data_catalog"] = dc_id + cr_json["research_dataset"]["issued"] = "2018-01-01" + cr_json["research_dataset"]["publisher"] = { + "@type": "Organization", + "name": {"en": "publisher"}, } - response = self.client.post('/rest/datasets?pid_type=urn', cr_json, format="json") + response = self.client.post("/rest/datasets?pid_type=urn", cr_json, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - identifier = response.data['identifier'] + identifier = response.data["identifier"] # Verify rpc api returns the same doi as the one that is set to the datasets' preservation identifier - response = self.client.post(f'/rpc/datasets/set_preservation_identifier?identifier={identifier}') + response = self.client.post( + f"/rpc/datasets/set_preservation_identifier?identifier={identifier}" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response2 = self.client.get(f'/rest/datasets/{identifier}') + response2 = self.client.get(f"/rest/datasets/{identifier}") self.assertEqual(response2.status_code, status.HTTP_200_OK, response2.data) - self.assertEqual(response.data, response2.data['preservation_identifier'], response2.data) + self.assertEqual(response.data, response2.data["preservation_identifier"], response2.data) # Return 400 if request is not correct datacite format - response2.data['research_dataset'].pop('issued') - response = self.client.put(f'/rest/datasets/{identifier}', response2.data, format="json") + response2.data["research_dataset"].pop("issued") + response = self.client.put(f"/rest/datasets/{identifier}", response2.data, format="json") self.assertEqual(response2.status_code, status.HTTP_200_OK, response2.data) - response = self.client.post(f'/rpc/datasets/set_preservation_identifier?identifier={identifier}') + response = self.client.post( + f"/rpc/datasets/set_preservation_identifier?identifier={identifier}" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) @@ -126,16 +154,16 @@ class ChangeCumulativeStateRPC(CatalogRecordApiWriteCommon): """ def _create_cumulative_dataset(self, state): - self.cr_test_data['cumulative_state'] = state + self.cr_test_data["cumulative_state"] = state - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['cumulative_state'], state, response.data) + self.assertEqual(response.data["cumulative_state"], state, response.data) return response.data def _update_cr_cumulative_state(self, identifier, state, result=status.HTTP_204_NO_CONTENT): - url = '/rpc/datasets/change_cumulative_state?identifier=%s&cumulative_state=%d' + url = "/rpc/datasets/change_cumulative_state?identifier=%s&cumulative_state=%d" response = self.client.post(url % (identifier, state), format="json") self.assertEqual(response.status_code, result, response.data) @@ -143,15 +171,17 @@ def _update_cr_cumulative_state(self, identifier, state, result=status.HTTP_204_ return response.data def _get_cr(self, identifier): - response = self.client.get('/rest/datasets/%s' % identifier, format="json") + response = self.client.get("/rest/datasets/%s" % identifier, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) return response.data def _assert_file_counts(self, new_version): - new_count = CatalogRecord.objects.get(pk=new_version['id']).files.count() - old_count = CatalogRecord.objects.get(pk=new_version['previous_dataset_version']['id']).files.count() - self.assertEqual(new_count, old_count, 'file count between versions should match') + new_count = CatalogRecord.objects.get(pk=new_version["id"]).files.count() + old_count = CatalogRecord.objects.get( + pk=new_version["previous_dataset_version"]["id"] + ).files.count() + self.assertEqual(new_count, old_count, "file count between versions should match") def test_transitions_from_NO(self): """ @@ -159,215 +189,233 @@ def test_transitions_from_NO(self): New version is created if non-cumulative dataset is marked actively cumulative. """ cr_orig = self._create_cumulative_dataset(0) - orig_preferred_identifier = cr_orig['research_dataset']['preferred_identifier'] + orig_preferred_identifier = cr_orig["research_dataset"]["preferred_identifier"] orig_record_count = CatalogRecord.objects.all().count() - self._update_cr_cumulative_state(cr_orig['identifier'], 2, status.HTTP_400_BAD_REQUEST) + self._update_cr_cumulative_state(cr_orig["identifier"], 2, status.HTTP_400_BAD_REQUEST) - self._update_cr_cumulative_state(cr_orig['identifier'], 1, status.HTTP_200_OK) + self._update_cr_cumulative_state(cr_orig["identifier"], 1, status.HTTP_200_OK) self.assertEqual(CatalogRecord.objects.all().count(), orig_record_count + 1) # get updated dataset - old_version = self._get_cr(cr_orig['identifier']) - self.assertEqual(old_version['cumulative_state'], 0, 'original status should not changed') - self.assertTrue('next_dataset_version' in old_version, 'should have new dataset') + old_version = self._get_cr(cr_orig["identifier"]) + self.assertEqual(old_version["cumulative_state"], 0, "original status should not changed") + self.assertTrue("next_dataset_version" in old_version, "should have new dataset") # cannot change old dataset cumulative_status - self._update_cr_cumulative_state(old_version['identifier'], 2, status.HTTP_400_BAD_REQUEST) + self._update_cr_cumulative_state(old_version["identifier"], 2, status.HTTP_400_BAD_REQUEST) # new version of the dataset should have new cumulative state - new_version = self._get_cr(old_version['next_dataset_version']['identifier']) - self.assertTrue(new_version['research_dataset']['preferred_identifier'] != orig_preferred_identifier) - self.assertEqual(new_version['cumulative_state'], 1, 'new version should have changed status') + new_version = self._get_cr(old_version["next_dataset_version"]["identifier"]) + self.assertTrue( + new_version["research_dataset"]["preferred_identifier"] != orig_preferred_identifier + ) + self.assertEqual( + new_version["cumulative_state"], 1, "new version should have changed status" + ) self._assert_file_counts(new_version) def test_transitions_from_YES(self): cr = self._create_cumulative_dataset(1) orig_record_count = CatalogRecord.objects.all().count() - self._update_cr_cumulative_state(cr['identifier'], 0, status.HTTP_400_BAD_REQUEST) + self._update_cr_cumulative_state(cr["identifier"], 0, status.HTTP_400_BAD_REQUEST) self.assertEqual(CatalogRecord.objects.all().count(), orig_record_count) # active to non-active cumulation is legal - self._update_cr_cumulative_state(cr['identifier'], 2) - cr = self._get_cr(cr['identifier']) - self.assertEqual(cr['cumulative_state'], 2, 'dataset should have changed status') + self._update_cr_cumulative_state(cr["identifier"], 2) + cr = self._get_cr(cr["identifier"]) + self.assertEqual(cr["cumulative_state"], 2, "dataset should have changed status") def test_correct_response_data(self): """ Tests that correct information is set to response. """ cr = self._create_cumulative_dataset(0) - return_data = self._update_cr_cumulative_state(cr['identifier'], 1, status.HTTP_200_OK) - self.assertTrue('new_version_created' in return_data, 'new_version_created should be returned') - new_version_identifier = return_data['new_version_created']['identifier'] - cr = self._get_cr(cr['identifier']) - self.assertEqual(cr['next_dataset_version']['identifier'], new_version_identifier) + return_data = self._update_cr_cumulative_state(cr["identifier"], 1, status.HTTP_200_OK) + self.assertTrue( + "new_version_created" in return_data, + "new_version_created should be returned", + ) + new_version_identifier = return_data["new_version_created"]["identifier"] + cr = self._get_cr(cr["identifier"]) + self.assertEqual(cr["next_dataset_version"]["identifier"], new_version_identifier) new_cr = self._get_cr(new_version_identifier) - return_data = self._update_cr_cumulative_state(new_cr['identifier'], 2) - self.assertEqual(return_data, None, 'when new version is not created, return should be None') + return_data = self._update_cr_cumulative_state(new_cr["identifier"], 2) + self.assertEqual( + return_data, None, "when new version is not created, return should be None" + ) class RefreshDirectoryContent(CatalogRecordApiWriteAssignFilesCommon): - url = '/rpc/datasets/refresh_directory_content?cr_identifier=%s&dir_identifier=%s' + url = "/rpc/datasets/refresh_directory_content?cr_identifier=%s&dir_identifier=%s" def _assert_rd_total_byte_size(self, file_size_before, file_size_after, expected_addition): self.assertEqual(file_size_after, file_size_before + expected_addition) def test_refresh_adds_new_files(self): - self._add_directory(self.cr_test_data, '/TestExperiment') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['identifier'] - dir_id = response.data['research_dataset']['directories'][0]['identifier'] - file_byte_size_before = response.data['research_dataset']['total_files_byte_size'] + cr_id = response.data["identifier"] + dir_id = response.data["research_dataset"]["directories"][0]["identifier"] + file_byte_size_before = response.data["research_dataset"]["total_files_byte_size"] # freeze two files to /TestExperiment/Directory_2 self._freeze_files_to_root() response = self.client.post(self.url % (cr_id, dir_id), format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['number_of_files_added'], 2) - - new_version = CatalogRecord.objects.get(id=response.data['new_version_created']['id']) - file_size_after = new_version.research_dataset['total_files_byte_size'] - self.assertEqual(new_version.files.count(), new_version.previous_dataset_version.files.count() + 2) - self._assert_rd_total_byte_size(file_byte_size_before, file_size_after, self._single_file_byte_size * 2) - - # freeze two files to /TestExperiment/Directory_2/Group_3 - self._freeze_new_files() - response = self.client.post(self.url % (new_version.identifier, dir_id), format="json") - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['number_of_files_added'], 2) - - new_version = CatalogRecord.objects.get(id=response.data['new_version_created']['id']) - self.assertEqual(new_version.files.count(), new_version.previous_dataset_version.files.count() + 2) + self.assertEqual(response.data["number_of_files_added"], 2) + + new_version = CatalogRecord.objects.get(id=response.data["new_version_created"]["id"]) + file_size_after = new_version.research_dataset["total_files_byte_size"] + self.assertEqual( + new_version.files.count(), + new_version.previous_dataset_version.files.count() + 2, + ) + self._assert_rd_total_byte_size( + file_byte_size_before, file_size_after, self._single_file_byte_size * 2 + ) def test_adding_parent_dir_allows_refreshes_to_child_dirs(self): """ When parent directory is added to dataset, refreshes to child directories are also possible. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['identifier'] + cr_id = response.data["identifier"] self._freeze_new_files() - frozen_dir = Directory.objects.filter(directory_path='/TestExperiment/Directory_2/Group_3').first() + frozen_dir = Directory.objects.filter( + directory_path="/TestExperiment/Directory_2/Group_3" + ).first() response = self.client.post(self.url % (cr_id, frozen_dir.identifier), format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['number_of_files_added'], 2) + self.assertEqual(response.data["number_of_files_added"], 2) - new_version = CatalogRecord.objects.get(id=response.data['new_version_created']['id']) - self.assertEqual(new_version.files.count(), new_version.previous_dataset_version.files.count() + 2) + new_version = CatalogRecord.objects.get(id=response.data["new_version_created"]["id"]) + self.assertEqual( + new_version.files.count(), + new_version.previous_dataset_version.files.count() + 2, + ) def test_refresh_adds_new_files_multiple_locations(self): - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['identifier'] - dir_id = response.data['research_dataset']['directories'][0]['identifier'] + cr_id = response.data["identifier"] + dir_id = response.data["research_dataset"]["directories"][0]["identifier"] self._freeze_new_files() self._freeze_files_to_root() response = self.client.post(self.url % (cr_id, dir_id), format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['number_of_files_added'], 4) + self.assertEqual(response.data["number_of_files_added"], 4) - new_version = CatalogRecord.objects.get(id=response.data['new_version_created']['id']) - self.assertEqual(new_version.files.count(), new_version.previous_dataset_version.files.count() + 4) + new_version = CatalogRecord.objects.get(id=response.data["new_version_created"]["id"]) + self.assertEqual( + new_version.files.count(), + new_version.previous_dataset_version.files.count() + 4, + ) def test_refresh_adds_no_new_files_from_upper_dirs(self): """ Include parent/subdir and freeze files to parent. Should be no changes in the dataset. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['identifier'] - dir_id = response.data['research_dataset']['directories'][0]['identifier'] + cr_id = response.data["identifier"] + dir_id = response.data["research_dataset"]["directories"][0]["identifier"] file_count_before = CatalogRecord.objects.get(identifier=cr_id).files.count() self._freeze_files_to_root() response = self.client.post(self.url % (cr_id, dir_id), format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['number_of_files_added'], 0) + self.assertEqual(response.data["number_of_files_added"], 0) cr_after = CatalogRecord.objects.get(identifier=cr_id) - self.assertEqual(cr_after.next_dataset_version, None, 'should not have new dataset version') - self.assertEqual(cr_after.files.count(), file_count_before, 'No new files should be added') + self.assertEqual(cr_after.next_dataset_version, None, "should not have new dataset version") + self.assertEqual(cr_after.files.count(), file_count_before, "No new files should be added") def test_refresh_with_cumulative_state_yes(self): """ When dataset has cumulation active, files are added to dataset but no new version is created. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - self.cr_test_data['cumulative_state'] = 1 - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + self.cr_test_data["cumulative_state"] = 1 + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['identifier'] - dir_id = response.data['research_dataset']['directories'][0]['identifier'] + cr_id = response.data["identifier"] + dir_id = response.data["research_dataset"]["directories"][0]["identifier"] file_count_before = CatalogRecord.objects.get(identifier=cr_id).files.count() - file_byte_size_before = response.data['research_dataset']['total_files_byte_size'] + file_byte_size_before = response.data["research_dataset"]["total_files_byte_size"] self._freeze_new_files() self._freeze_files_to_root() response = self.client.post(self.url % (cr_id, dir_id), format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['number_of_files_added'], 4) + self.assertEqual(response.data["number_of_files_added"], 4) cr_after = CatalogRecord.objects.get(identifier=cr_id) - file_size_after = cr_after.research_dataset['total_files_byte_size'] - self.assertEqual(cr_after.next_dataset_version, None, 'should not have new dataset version') - self.assertEqual(len(cr_after.get_metadata_version_listing()), 2, 'new metadata version should be created') + file_size_after = cr_after.research_dataset["total_files_byte_size"] + self.assertEqual(cr_after.next_dataset_version, None, "should not have new dataset version") + self.assertEqual( + len(cr_after.get_metadata_version_listing()), + 2, + "new metadata version should be created", + ) self.assertEqual(cr_after.files.count(), file_count_before + 4) - self._assert_rd_total_byte_size(file_byte_size_before, file_size_after, self._single_file_byte_size * 4) + self._assert_rd_total_byte_size( + file_byte_size_before, file_size_after, self._single_file_byte_size * 4 + ) # check that added sub dir is found in catalog records internal variables - new_dir = \ - Directory.objects\ - .filter(directory_path__startswith='/TestExperiment/Directory_2/Group_3')\ - .first() - - self.assertTrue(str(new_dir.id) in cr_after._directory_data, 'New dir id should be found in cr') + new_dir = Directory.objects.filter( + directory_path__startswith="/TestExperiment/Directory_2/Group_3" + ).first() + + self.assertTrue( + str(new_dir.id) in cr_after._directory_data, + "New dir id should be found in cr", + ) self.assertEqual(new_dir.byte_size, self._single_file_byte_size * 2) def test_refreshing_deprecated_dataset_is_not_allowed(self): - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['identifier'] - dir_id = response.data['research_dataset']['directories'][0]['identifier'] + cr_id = response.data["identifier"] + dir_id = response.data["research_dataset"]["directories"][0]["identifier"] removed_file_id = CatalogRecord.objects.get(identifier=cr_id).files.all()[0].id - response = self.client.delete(f'/rest/files/{removed_file_id}') + response = self.client.delete(f"/rest/files/{removed_file_id}") self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.get(f'/rest/datasets/{cr_id}') + response = self.client.get(f"/rest/datasets/{cr_id}") self.assertEqual(response.status_code, status.HTTP_200_OK) depr_cr = response.data self._freeze_new_files() - response = self.client.post(self.url % (depr_cr['identifier'], dir_id), format="json") + response = self.client.post(self.url % (depr_cr["identifier"], dir_id), format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) def test_adding_files_from_non_assigned_dir_is_not_allowed(self): """ Only allow adding files from directories which paths are included in the research dataset. """ - self._add_directory(self.cr_test_data, '/SecondExperiment/Data') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['identifier'] - - # create another dataset so that dir /SecondExperiment/Data_Config will be created - self._add_directory(self.cr_test_data, '/SecondExperiment/Data_Config') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/SecondExperiment/Data") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - dir_id = response.data['research_dataset']['directories'][1]['identifier'] + cr_id = response.data["identifier"] + dir_id = Directory.objects.filter( + directory_path="/SecondExperiment/Data_Config" + ).first().identifier response = self.client.post(self.url % (cr_id, dir_id), format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('not included' in response.data['detail'][0], response.data) + self.assertTrue("not included" in response.data["detail"][0], response.data) class FixDeprecatedTests(CatalogRecordApiWriteAssignFilesCommon): @@ -380,10 +428,12 @@ def _get_next_dataset_version(self, identifier): """ Returns next dataset version for dataset """ - response = self.client.get('/rest/datasets/%s' % identifier) + response = self.client.get("/rest/datasets/%s" % identifier) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue('next_dataset_version' in response.data, 'new dataset should be created') - response = self.client.get('/rest/datasets/%s' % response.data['next_dataset_version']['identifier']) + self.assertTrue("next_dataset_version" in response.data, "new dataset should be created") + response = self.client.get( + "/rest/datasets/%s" % response.data["next_dataset_version"]["identifier"] + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) return response.data @@ -394,12 +444,17 @@ def _check_new_dataset_version(self, identifier, file_count_before, deleted_file files included. Research_dataset must be checked separately. """ new_cr_version = self._get_next_dataset_version(identifier) - new_version_files = CatalogRecord.objects.get(pk=new_cr_version['id']).files.all() - self.assertEqual(new_cr_version['deprecated'], False, 'deprecated flag should be fixed') - self.assertEqual(new_version_files.count(), file_count_before - len(deleted_file_ids), - 'new file count should be on smaller than before') - self.assertTrue(all(d not in new_version_files.values_list('id', flat=True) for d in deleted_file_ids), - 'Deleted files should not be found in new version') + new_version_files = CatalogRecord.objects.get(pk=new_cr_version["id"]).files.all() + self.assertEqual(new_cr_version["deprecated"], False, "deprecated flag should be fixed") + self.assertEqual( + new_version_files.count(), + file_count_before - len(deleted_file_ids), + "new file count should be on smaller than before", + ) + self.assertTrue( + all(d not in new_version_files.values_list("id", flat=True) for d in deleted_file_ids), + "Deleted files should not be found in new version", + ) return new_cr_version @@ -407,10 +462,11 @@ def _delete_files_from_directory_path(self, path): """ Deletes files from sub directories as well """ - deleted_file_ids = [ id for id in File.objects - .filter(file_path__startswith=path) - .values_list('id', flat=True) ] - response = self.client.delete('/rest/files', deleted_file_ids, format="json") + deleted_file_ids = [ + id + for id in File.objects.filter(file_path__startswith=path).values_list("id", flat=True) + ] + response = self.client.delete("/rest/files", deleted_file_ids, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) return deleted_file_ids @@ -420,129 +476,187 @@ def test_fix_deprecated_files(self): # delete file from dataset deleted_file = File.objects.get(pk=1) - response = self.client.delete('/rest/files/%s' % deleted_file.identifier) + response = self.client.delete("/rest/files/%s" % deleted_file.identifier) self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response.data['deprecated'], 'dataset should be deprecated') - identifier = response.data['identifier'] + self.assertTrue(response.data["deprecated"], "dataset should be deprecated") + identifier = response.data["identifier"] # fix deprecated dataset - response = self.client.post('/rpc/datasets/fix_deprecated?identifier=%s' % identifier) + response = self.client.post("/rpc/datasets/fix_deprecated?identifier=%s" % identifier) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # ensure that new dataset version is not deprecated, dataset files contain only the non-removed file # and removed file is deleted from research_dataset - new_cr_version = self._check_new_dataset_version(identifier, file_count_before, [deleted_file.id]) - rd_filenames = [ f['identifier'] for f in new_cr_version['research_dataset']['files'] ] - self.assertTrue(deleted_file.identifier not in rd_filenames, 'deleted file should not be in research_dataset') + new_cr_version = self._check_new_dataset_version( + identifier, file_count_before, [deleted_file.id] + ) + rd_filenames = [f["identifier"] for f in new_cr_version["research_dataset"]["files"]] + self.assertTrue( + deleted_file.identifier not in rd_filenames, + "deleted file should not be in research_dataset", + ) def test_fix_deprecated_directories(self): """ This test adds parent directory of two files to a dataset and deletes the files """ # add/describe the parent directory of the newly added file to the dataset - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr_with_dir = response.data - file_count_before = CatalogRecord.objects.get(identifier=cr_with_dir['identifier']).files.count() + file_count_before = CatalogRecord.objects.get( + identifier=cr_with_dir["identifier"] + ).files.count() # delete/unfreeze the files contained by described directory - deleted_file_ids = self._delete_files_from_directory_path('/TestExperiment/Directory_2/Group_2/Group_2_deeper') + deleted_file_ids = self._delete_files_from_directory_path( + "/TestExperiment/Directory_2/Group_2/Group_2_deeper" + ) # fix deprecated dataset - response = self.client.post('/rpc/datasets/fix_deprecated?identifier=%s' % cr_with_dir['identifier']) + response = self.client.post( + "/rpc/datasets/fix_deprecated?identifier=%s" % cr_with_dir["identifier"] + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # ensure old dataset is unchanged - response = self.client.get('/rest/datasets/%s' % cr_with_dir['identifier']) + response = self.client.get("/rest/datasets/%s" % cr_with_dir["identifier"]) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data['research_dataset']['directories']), 1, - 'old dataset version directories should still contain the removed directory') - self.assertTrue(response.data['deprecated'], 'old dataset version deprecated flag should not be changed') + self.assertEqual( + len(response.data["research_dataset"]["directories"]), + 1, + "old dataset version directories should still contain the removed directory", + ) + self.assertTrue( + response.data["deprecated"], + "old dataset version deprecated flag should not be changed", + ) # ensure the new dataset is correct - new_cr_version = self._check_new_dataset_version(cr_with_dir['identifier'], file_count_before, - deleted_file_ids) - self.assertTrue('directories' not in new_cr_version['research_dataset']) + new_cr_version = self._check_new_dataset_version( + cr_with_dir["identifier"], file_count_before, deleted_file_ids + ) + self.assertTrue("directories" not in new_cr_version["research_dataset"]) def test_fix_deprecated_nested_directories_1(self): """ This test adds parent directory to dataset and then deletes all files from sub directory. research_dataset should be unchanged and file count should be smaller for new version. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr_before = response.data - file_count_before = CatalogRecord.objects.get(identifier=cr_before['identifier']).files.count() + file_count_before = CatalogRecord.objects.get( + identifier=cr_before["identifier"] + ).files.count() # delete/unfreeze the files contained by described directory - deleted_file_ids = self._delete_files_from_directory_path('/TestExperiment/Directory_2/Group_2/Group_2_deeper') + deleted_file_ids = self._delete_files_from_directory_path( + "/TestExperiment/Directory_2/Group_2/Group_2_deeper" + ) # fix deprecated dataset - response = self.client.post('/rpc/datasets/fix_deprecated?identifier=%s' % cr_before['identifier']) + response = self.client.post( + "/rpc/datasets/fix_deprecated?identifier=%s" % cr_before["identifier"] + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # ensure the new dataset is correct - new_cr_version = self._check_new_dataset_version(cr_before['identifier'], file_count_before, deleted_file_ids) - self.assertEqual(cr_before['research_dataset'].get('files'), - new_cr_version['research_dataset'].get('files'), 'should be no difference in research_dataset.files') - self.assertEqual(cr_before['research_dataset'].get('directories'), - new_cr_version['research_dataset'].get('directories'), 'should be no difference in research_dataset.dirs') + new_cr_version = self._check_new_dataset_version( + cr_before["identifier"], file_count_before, deleted_file_ids + ) + self.assertEqual( + cr_before["research_dataset"].get("files"), + new_cr_version["research_dataset"].get("files"), + "should be no difference in research_dataset.files", + ) + self.assertEqual( + cr_before["research_dataset"].get("directories"), + new_cr_version["research_dataset"].get("directories"), + "should be no difference in research_dataset.dirs", + ) def test_fix_deprecated_nested_directories_2(self): """ This test adds parent and sub directory to dataset and then deletes all files from sub directory. research_dataset and file count should change for new version. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2') - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2/Group_2_deeper") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr_before = response.data - file_count_before = CatalogRecord.objects.get(identifier=cr_before['identifier']).files.count() + file_count_before = CatalogRecord.objects.get( + identifier=cr_before["identifier"] + ).files.count() - deleted_file_ids = self._delete_files_from_directory_path('/TestExperiment/Directory_2/Group_2/Group_2_deeper') + deleted_file_ids = self._delete_files_from_directory_path( + "/TestExperiment/Directory_2/Group_2/Group_2_deeper" + ) # fix deprecated dataset - response = self.client.post('/rpc/datasets/fix_deprecated?identifier=%s' % cr_before['identifier']) + response = self.client.post( + "/rpc/datasets/fix_deprecated?identifier=%s" % cr_before["identifier"] + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # ensure the new dataset is correct - new_cr_version = self._check_new_dataset_version(cr_before['identifier'], file_count_before, deleted_file_ids) + new_cr_version = self._check_new_dataset_version( + cr_before["identifier"], file_count_before, deleted_file_ids + ) # description field conveniently has the dir path for directories which are saved by _add_directory - rd_dirpaths = [ d['description'] for d in new_cr_version['research_dataset']['directories'] ] - self.assertTrue('/TestExperiment/Directory_2/Group_2/Group_2_deeper' not in rd_dirpaths) + rd_dirpaths = [d["description"] for d in new_cr_version["research_dataset"]["directories"]] + self.assertTrue("/TestExperiment/Directory_2/Group_2/Group_2_deeper" not in rd_dirpaths) def test_fix_deprecated_nested_directories_3(self): """ This test adds parent and sub directory to dataset and then deletes all files from sub directory. research_dataset and file count should change for new version. """ - self._add_directory(self.cr_test_data, '/TestExperiment/Directory_2/Group_2') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_12.txt') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/file_09.txt') - self._add_file(self.cr_test_data, '/TestExperiment/Directory_2/Group_2/file_10.txt') - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + self._add_directory(self.cr_test_data, "/TestExperiment/Directory_2/Group_2") + self._add_file( + self.cr_test_data, + "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt", + ) + self._add_file( + self.cr_test_data, + "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_12.txt", + ) + self._add_file(self.cr_test_data, "/TestExperiment/Directory_2/Group_2/file_09.txt") + self._add_file(self.cr_test_data, "/TestExperiment/Directory_2/Group_2/file_10.txt") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) cr_before = response.data - file_count_before = CatalogRecord.objects.get(identifier=cr_before['identifier']).files.count() + file_count_before = CatalogRecord.objects.get( + identifier=cr_before["identifier"] + ).files.count() # delete/unfreeze the files contained by described directory - deleted_file_ids = self._delete_files_from_directory_path('/TestExperiment/Directory_2/Group_2/Group_2_deeper') + deleted_file_ids = self._delete_files_from_directory_path( + "/TestExperiment/Directory_2/Group_2/Group_2_deeper" + ) # fix deprecated dataset - response = self.client.post('/rpc/datasets/fix_deprecated?identifier=%s' % cr_before['identifier']) + response = self.client.post( + "/rpc/datasets/fix_deprecated?identifier=%s" % cr_before["identifier"] + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # ensure the new dataset is correct - new_cr_version = self._check_new_dataset_version(cr_before['identifier'], file_count_before, deleted_file_ids) - rd_dirpaths = [ d['description'] for d in new_cr_version['research_dataset']['directories'] ] - rd_filepaths = [ f['description'] for f in new_cr_version['research_dataset']['files'] ] - self.assertTrue('/TestExperiment/Directory_2/Group_2/Group_2_deeper' not in rd_dirpaths) - self.assertTrue('/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt' not in rd_filepaths) - self.assertTrue('/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_12.txt' not in rd_filepaths) + new_cr_version = self._check_new_dataset_version( + cr_before["identifier"], file_count_before, deleted_file_ids + ) + rd_dirpaths = [d["description"] for d in new_cr_version["research_dataset"]["directories"]] + rd_filepaths = [f["description"] for f in new_cr_version["research_dataset"]["files"]] + self.assertTrue("/TestExperiment/Directory_2/Group_2/Group_2_deeper" not in rd_dirpaths) + self.assertTrue( + "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_11.txt" not in rd_filepaths + ) + self.assertTrue( + "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_12.txt" not in rd_filepaths + ) diff --git a/src/metax_api/tests/api/rpc/base/views/file_rpc.py b/src/metax_api/tests/api/rpc/base/views/file_rpc.py index ab780219..1e22da54 100755 --- a/src/metax_api/tests/api/rpc/base/views/file_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/file_rpc.py @@ -14,15 +14,15 @@ class FileRPCTests(APITestCase, TestClassUtils): - def setUp(self): """ Reloaded for every test case """ super().setUp() - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) self._use_http_authorization() + class DeleteProjectTests(FileRPCTests): """ @@ -32,39 +32,49 @@ class DeleteProjectTests(FileRPCTests): def test_wrong_parameters(self): # correct user, no project identifier - response = self.client.post('/rpc/files/delete_project') + response = self.client.post("/rpc/files/delete_project") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) # nonexisting project identifier: - response = self.client.post('/rpc/files/delete_project?project_identifier=non_existing') - self.assertEqual(response.data['deleted_files_count'], 0) + response = self.client.post("/rpc/files/delete_project?project_identifier=non_existing") + self.assertEqual(response.data["deleted_files_count"], 0) # wrong request method - response = self.client.delete('/rpc/files/delete_project?project_identifier=research_project_112') + response = self.client.delete( + "/rpc/files/delete_project?project_identifier=research_project_112" + ) self.assertEqual(response.status_code, 501) # wrong user - self._use_http_authorization('api_auth_user') - response = self.client.post('/rpc/files/delete_project?project_identifier=research_project_112') + self._use_http_authorization("api_auth_user") + response = self.client.post( + "/rpc/files/delete_project?project_identifier=research_project_112" + ) # self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_known_project_identifier(self): - response = self.client.post('/rpc/files/delete_project?project_identifier=research_project_112') + response = self.client.post( + "/rpc/files/delete_project?project_identifier=research_project_112" + ) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_files_are_marked_deleted(self): - files_count_before = File.objects.filter(project_identifier='research_project_112').count() - response = self.client.post('/rpc/files/delete_project?project_identifier=research_project_112') - self.assertEqual(files_count_before, response.data['deleted_files_count']) + files_count_before = File.objects.filter(project_identifier="research_project_112").count() + response = self.client.post( + "/rpc/files/delete_project?project_identifier=research_project_112" + ) + self.assertEqual(files_count_before, response.data["deleted_files_count"]) def test_directories_are_deleted(self): - self.client.post('/rpc/files/delete_project?project_identifier=research_project_112') - directories_count_after = Directory.objects.filter(project_identifier='research_project_112').count() + self.client.post("/rpc/files/delete_project?project_identifier=research_project_112") + directories_count_after = Directory.objects.filter( + project_identifier="research_project_112" + ).count() self.assertEqual(directories_count_after, 0) def test_datasets_are_marked_deprecated(self): - file_ids = File.objects.filter(project_identifier='project_x').values_list('id', flat=True) - related_dataset = CatalogRecord.objects.filter(files__in=file_ids).distinct('id')[0] - self.client.post('/rpc/files/delete_project?project_identifier=project_x') - response = self.client.get('/rest/datasets/%s' % related_dataset.identifier) - self.assertEqual(response.data['deprecated'], True) + file_ids = File.objects.filter(project_identifier="project_x").values_list("id", flat=True) + related_dataset = CatalogRecord.objects.filter(files__in=file_ids).distinct("id")[0] + self.client.post("/rpc/files/delete_project?project_identifier=project_x") + response = self.client.get("/rest/datasets/%s" % related_dataset.identifier) + self.assertEqual(response.data["deprecated"], True) diff --git a/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py b/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py index 623f4302..e330e325 100755 --- a/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py @@ -20,23 +20,22 @@ class StatisticRPCCommon(APITestCase, TestClassUtils): - @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ super().setUpClass() - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) def setUp(self): super().setUp() - self._use_http_authorization(username='metax') + self._use_http_authorization(username="metax") self._setup_testdata() def _setup_testdata(self): - test_orgs = ['org_1', 'org_2', 'org_3'] - access_types = [ v for k, v in ACCESS_TYPES.items() ] + test_orgs = ["org_1", "org_2", "org_3"] + access_types = [v for k, v in ACCESS_TYPES.items()] # ensure testdata has something sensible to test against. # todo may be feasible to move these to generate_test_data without breaking all tests ? @@ -53,72 +52,74 @@ def _setup_testdata(self): # distribute records between access types if cr.id % 5 == 0: - cr.research_dataset['access_rights']['access_type']['identifier'] = access_types[4] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = access_types[4] elif cr.id % 4 == 0: - cr.research_dataset['access_rights']['access_type']['identifier'] = access_types[3] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = access_types[3] elif cr.id % 3 == 0: - cr.research_dataset['access_rights']['access_type']['identifier'] = access_types[2] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = access_types[2] elif cr.id % 2 == 0: - cr.research_dataset['access_rights']['access_type']['identifier'] = access_types[1] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = access_types[1] else: - cr.research_dataset['access_rights']['access_type']['identifier'] = access_types[0] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = access_types[0] # distribute records between some creation months - date = '20%s-%s-13' + date = "20%s-%s-13" if cr.id % 8 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('18', '06')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("18", "06")) elif cr.id % 7 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('18', '07')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("18", "07")) elif cr.id % 6 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('18', '10')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("18", "10")) elif cr.id % 5 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('18', '11')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("18", "11")) elif cr.id % 4 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('18', '12')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("18", "12")) elif cr.id % 3 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('19', '01')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("19", "01")) elif cr.id % 2 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('19', '02')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("19", "02")) else: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('19', '03')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("19", "03")) # set some records as "created through end user api" if cr.id % 10 == 0: cr.service_created = None - cr.user_created = 'abc%d@fairdataid' % cr.id + cr.user_created = "abc%d@fairdataid" % cr.id cr.force_save() # create a few files which do not belong to any datasets - response = self.client.get('/rest/files/1', format='json') + response = self.client.get("/rest/files/1", format="json") files = [] for i in range(5): f = deepcopy(response.data) - del f['id'] - f['identifier'] += 'unique' + str(i) - f['file_name'] += str(i) - f['file_path'] += str(i) - f['project_identifier'] = 'prj123' + del f["id"] + f["identifier"] += "unique" + str(i) + f["file_name"] += str(i) + f["file_path"] += str(i) + f["project_identifier"] = "prj123" files.append(f) - response = self.client.post('/rest/files', files, format='json') + response = self.client.post("/rest/files", files, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - response = self.client.get('/rest/directories/update_byte_sizes_and_file_counts', format='json') + response = self.client.get( + "/rest/directories/update_byte_sizes_and_file_counts", format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/datasets/update_cr_total_files_byte_sizes', format='json') + response = self.client.get("/rest/datasets/update_cr_total_files_byte_sizes", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # create legacy datacatalog - dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema='ida').first() - dc.catalog_json['identifier'] = settings.LEGACY_CATALOGS[0] + dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema="ida").first() + dc.catalog_json["identifier"] = settings.LEGACY_CATALOGS[0] dc_json = { "catalog_record_services_create": "testuser,api_auth_user,metax", "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_json": dc.catalog_json + "catalog_json": dc.catalog_json, } - response = self.client.post('/rest/datacatalogs', dc_json, format="json") + response = self.client.post("/rest/datacatalogs", dc_json, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) def _set_deprecated_dataset(self, id=1): @@ -136,16 +137,12 @@ def _create_legacy_dataset(self): Creates one new legacy dataset and returns its id """ legacy_dataset = { - "data_catalog": { - "identifier": settings.LEGACY_CATALOGS[0] - }, + "data_catalog": {"identifier": settings.LEGACY_CATALOGS[0]}, "metadata_owner_org": "some_org_id", "metadata_provider_org": "some_org_id", "metadata_provider_user": "some_user_id", "research_dataset": { - "title": { - "en": "Test Dataset Title" - }, + "title": {"en": "Test Dataset Title"}, "description": { "en": "A descriptive description describing the contents of this dataset. Must be descriptive." }, @@ -154,54 +151,44 @@ def _create_legacy_dataset(self): "identifier": "pid:urn:1", "title": "File Title", "description": "informative description", - "use_category": { - "identifier": "method" - } + "use_category": {"identifier": "method"}, }, { "identifier": "pid:urn:3", "title": "File Title", "description": "informative description", - "use_category": { - "identifier": "method" - } - } + "use_category": {"identifier": "method"}, + }, ], "creator": [ { "name": "Teppo Testaaja", "@type": "Person", "member_of": { - "name": { - "fi": "Testiorganisaatio" - }, - "@type": "Organization" - } + "name": {"fi": "Testiorganisaatio"}, + "@type": "Organization", + }, } ], "access_rights": { "access_type": { "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", - "pref_label": { - "fi": "Avoin", - "en": "Open", - "und": "Avoin" - }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type" + "pref_label": {"fi": "Avoin", "en": "Open", "und": "Avoin"}, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", } }, - "preferred_identifier": "uniikkinen_aidentifaijeri" - } + "preferred_identifier": "uniikkinen_aidentifaijeri", + }, } - response = self.client.post('/rest/datasets', legacy_dataset, format="json") + response = self.client.post("/rest/datasets", legacy_dataset, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - return response.data['id'] + return response.data["id"] def _create_new_dataset(self, dataset_json): - response = self.client.post('/rest/datasets', dataset_json, format="json") + response = self.client.post("/rest/datasets", dataset_json, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - new_cr_id = response.data['id'] + new_cr_id = response.data["id"] return new_cr_id @@ -210,16 +197,16 @@ def _create_new_dataset_version(self, id=1): Finds the latest version of given dataset, deletes one file from it and updates. Does not check if there are files to be deleted. """ - cr = self.client.get(f'/rest/datasets/{id}?include_legacy', format='json').data - while cr.get('next_dataset_version', False): - id = cr['next_dataset_version']['id'] - cr = self.client.get(f'/rest/datasets/{id}?include_legacy', format='json').data + cr = self.client.get(f"/rest/datasets/{id}?include_legacy", format="json").data + while cr.get("next_dataset_version", False): + id = cr["next_dataset_version"]["id"] + cr = self.client.get(f"/rest/datasets/{id}?include_legacy", format="json").data - cr['research_dataset']['files'].pop() - response = self.client.put(f'/rest/datasets/{id}?include_legacy', cr, format="json") + cr["research_dataset"]["files"].pop() + response = self.client.put(f"/rest/datasets/{id}?include_legacy", cr, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - return response.data['next_dataset_version']['id'] + return response.data["next_dataset_version"]["id"] def _set_dataset_creation_date(self, cr_id, date): """ @@ -234,10 +221,9 @@ def _get_catalog_record_size(self, id): """ Returns the size of given record id """ - size = CatalogRecord\ - .objects_unfiltered\ - .get(id=id)\ - .research_dataset.get('total_files_byte_size', 0) + size = CatalogRecord.objects_unfiltered.get(id=id).research_dataset.get( + "total_files_byte_size", 0 + ) return size @@ -246,7 +232,7 @@ def _get_byte_size_of_month(self, date): Returns the byte size for given date. date is in format 'YYYY-MM' """ query = CatalogRecord.objects_unfiltered.filter(date_created__startswith=date) - list_of_sizes = [ cr.research_dataset.get('total_files_byte_size', 0) for cr in query ] + list_of_sizes = [cr.research_dataset.get("total_files_byte_size", 0) for cr in query] return sum(list_of_sizes) @@ -255,7 +241,7 @@ def _get_total_byte_size(self): Returns byte size of all datasets in database """ query = CatalogRecord.objects_unfiltered.all() - list_of_sizes = [cr.research_dataset.get('total_files_byte_size', 0) for cr in query] + list_of_sizes = [cr.research_dataset.get("total_files_byte_size", 0) for cr in query] return sum(list_of_sizes) @@ -269,7 +255,9 @@ def _get_dataset_count_after(self, date): """ Return the total count after the date provided (inclusive). date is in format 'YYYY-MM-DD' """ - return CatalogRecord.objects_unfiltered.filter(date_created__gte=f'{date}T00:00:00+03:00').count() + return CatalogRecord.objects_unfiltered.filter( + date_created__gte=f"{date}T00:00:00+03:00" + ).count() def _get_total_dataset_count(self): """ @@ -284,7 +272,7 @@ def _set_cr_datacatalog(self, cr_id, catalog_id): def _set_dataset_as_draft(self, cr_id): cr = CatalogRecord.objects.get(pk=cr_id) - cr.state = 'draft' + cr.state = "draft" cr.force_save() def _set_cr_organization(self, cr_id, org): @@ -304,37 +292,39 @@ def test_count_datasets_single(self): Tests single parameters for api. Empty removed and legacy parameters returns true AND false matches """ total_count = CatalogRecord.objects_unfiltered.count() - response = self.client.get('/rpc/statistics/count_datasets').data - self.assertEqual(total_count, response['count'], response) + response = self.client.get("/rpc/statistics/count_datasets").data + self.assertEqual(total_count, response["count"], response) # test removed -parameter self._set_removed_dataset(id=2) - response = self.client.get('/rpc/statistics/count_datasets?removed=true').data - self.assertEqual(response['count'], 1, response) + response = self.client.get("/rpc/statistics/count_datasets?removed=true").data + self.assertEqual(response["count"], 1, response) - response = self.client.get('/rpc/statistics/count_datasets?removed=false').data - self.assertEqual(response['count'], total_count - 1, response) + response = self.client.get("/rpc/statistics/count_datasets?removed=false").data + self.assertEqual(response["count"], total_count - 1, response) # test legacy -parameter self._create_legacy_dataset() total_count = CatalogRecord.objects_unfiltered.count() - response = self.client.get('/rpc/statistics/count_datasets?legacy=true').data - self.assertEqual(response['count'], 1, response) + response = self.client.get("/rpc/statistics/count_datasets?legacy=true").data + self.assertEqual(response["count"], 1, response) - response = self.client.get('/rpc/statistics/count_datasets?legacy=false').data - self.assertEqual(response['count'], total_count - 1, response) + response = self.client.get("/rpc/statistics/count_datasets?legacy=false").data + self.assertEqual(response["count"], total_count - 1, response) # test latest -parameter self._create_new_dataset_version() self._create_new_dataset_version() total_count = CatalogRecord.objects_unfiltered.count() - response = self.client.get('/rpc/statistics/count_datasets?latest=false').data # returns all - self.assertEqual(response['count'], total_count, response) + response = self.client.get( + "/rpc/statistics/count_datasets?latest=false" + ).data # returns all + self.assertEqual(response["count"], total_count, response) - with_param = self.client.get('/rpc/statistics/count_datasets?latest=true').data - without_param = self.client.get('/rpc/statistics/count_datasets').data # default is true - self.assertEqual(with_param['count'], total_count - 2, with_param) - self.assertEqual(with_param['count'], without_param['count'], with_param) + with_param = self.client.get("/rpc/statistics/count_datasets?latest=true").data + without_param = self.client.get("/rpc/statistics/count_datasets").data # default is true + self.assertEqual(with_param["count"], total_count - 2, with_param) + self.assertEqual(with_param["count"], without_param["count"], with_param) def test_count_datasets_removed_latest(self): second_ver = self._create_new_dataset_version() @@ -343,21 +333,27 @@ def test_count_datasets_removed_latest(self): self._set_removed_dataset(id=second_ver) self._set_removed_dataset(id=2) - rem_lat = self.client.get('/rpc/statistics/count_datasets?removed=true&latest=true').data - rem_not_lat = self.client.get('/rpc/statistics/count_datasets?removed=true&latest=false').data + rem_lat = self.client.get("/rpc/statistics/count_datasets?removed=true&latest=true").data + rem_not_lat = self.client.get( + "/rpc/statistics/count_datasets?removed=true&latest=false" + ).data - self.assertEqual(rem_lat['count'], 1, 'Only latest versions should be checked') # id=2 - self.assertEqual(rem_not_lat['count'], 3, 'Only the prev versions should be removed') + self.assertEqual(rem_lat["count"], 1, "Only latest versions should be checked") # id=2 + self.assertEqual(rem_not_lat["count"], 3, "Only the prev versions should be removed") # create new dataset with 2 versions - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self._create_new_dataset_version(response.data['id']) + self._create_new_dataset_version(response.data["id"]) - not_rem_lat = self.client.get('/rpc/statistics/count_datasets?removed=false&latest=true').data - not_rem_not_lat = self.client.get('/rpc/statistics/count_datasets?removed=false&latest=false').data + not_rem_lat = self.client.get( + "/rpc/statistics/count_datasets?removed=false&latest=true" + ).data + not_rem_not_lat = self.client.get( + "/rpc/statistics/count_datasets?removed=false&latest=false" + ).data - self.assertEqual(not_rem_lat['count'], not_rem_not_lat['count'] - 1) + self.assertEqual(not_rem_lat["count"], not_rem_not_lat["count"] - 1) def test_count_datasets_removed_legacy(self): self._create_legacy_dataset() @@ -366,15 +362,21 @@ def test_count_datasets_removed_legacy(self): self._set_removed_dataset(leg_cr) total_count = CatalogRecord.objects_unfiltered.count() - rem_leg = self.client.get('/rpc/statistics/count_datasets?removed=true&legacy=true').data - rem_not_leg = self.client.get('/rpc/statistics/count_datasets?removed=true&legacy=false').data - not_rem_leg = self.client.get('/rpc/statistics/count_datasets?removed=false&legacy=true').data - not_rem_not_leg = self.client.get('/rpc/statistics/count_datasets?removed=false&legacy=false').data - - self.assertEqual(rem_leg['count'], 1) - self.assertEqual(rem_not_leg['count'], 0) - self.assertEqual(not_rem_leg['count'], 2) - self.assertEqual(not_rem_not_leg['count'], total_count - 3) + rem_leg = self.client.get("/rpc/statistics/count_datasets?removed=true&legacy=true").data + rem_not_leg = self.client.get( + "/rpc/statistics/count_datasets?removed=true&legacy=false" + ).data + not_rem_leg = self.client.get( + "/rpc/statistics/count_datasets?removed=false&legacy=true" + ).data + not_rem_not_leg = self.client.get( + "/rpc/statistics/count_datasets?removed=false&legacy=false" + ).data + + self.assertEqual(rem_leg["count"], 1) + self.assertEqual(rem_not_leg["count"], 0) + self.assertEqual(not_rem_leg["count"], 2) + self.assertEqual(not_rem_not_leg["count"], total_count - 3) def test_count_datasets_latest_legacy(self): leg_cr = self._create_legacy_dataset() @@ -382,46 +384,51 @@ def test_count_datasets_latest_legacy(self): self._create_new_dataset_version(leg_cr) total_count = CatalogRecord.objects_unfiltered.count() - leg_lat = self.client.get('/rpc/statistics/count_datasets?legacy=true&latest=true').data - leg_not_lat = self.client.get('/rpc/statistics/count_datasets?legacy=true&latest=false').data - not_leg_not_lat = self.client.get('/rpc/statistics/count_datasets?legacy=false&latest=false').data + leg_lat = self.client.get("/rpc/statistics/count_datasets?legacy=true&latest=true").data + leg_not_lat = self.client.get( + "/rpc/statistics/count_datasets?legacy=true&latest=false" + ).data + not_leg_not_lat = self.client.get( + "/rpc/statistics/count_datasets?legacy=false&latest=false" + ).data - self.assertEqual(leg_lat['count'], 1) - self.assertEqual(leg_not_lat['count'], 3) - self.assertEqual(not_leg_not_lat['count'], total_count - 3) + self.assertEqual(leg_lat["count"], 1) + self.assertEqual(leg_not_lat["count"], 3) + self.assertEqual(not_leg_not_lat["count"], total_count - 3) def test_count_datasets_from_date(self): total_count = CatalogRecord.objects_unfiltered.count() - june_count = self._get_dataset_count_of_month('2018-06') - july_count = self._get_dataset_count_of_month('2018-07') + june_count = self._get_dataset_count_of_month("2018-06") + july_count = self._get_dataset_count_of_month("2018-07") - res = self.client.get('/rpc/statistics/count_datasets?from_date=2018-07-01').data - self.assertEqual(res['count'], total_count - june_count) + res = self.client.get("/rpc/statistics/count_datasets?from_date=2018-07-01").data + self.assertEqual(res["count"], total_count - june_count) # datasets are created on 13th so this should include august count - res = self.client.get('/rpc/statistics/count_datasets?from_date=2018-08-13').data - self.assertEqual(res['count'], total_count - june_count - july_count) + res = self.client.get("/rpc/statistics/count_datasets?from_date=2018-08-13").data + self.assertEqual(res["count"], total_count - june_count - july_count) def test_count_datasets_to_date(self): total_count = CatalogRecord.objects_unfiltered.count() - after_jan_count = self._get_dataset_count_after('2019-01-01') - after_feb_count = self._get_dataset_count_after('2019-02-14') + after_jan_count = self._get_dataset_count_after("2019-01-01") + after_feb_count = self._get_dataset_count_after("2019-02-14") + + res = self.client.get("/rpc/statistics/count_datasets?to_date=2019-01-01").data + self.assertEqual(res["count"], total_count - after_jan_count) - res = self.client.get('/rpc/statistics/count_datasets?to_date=2019-01-01').data - self.assertEqual(res['count'], total_count - after_jan_count) + res = self.client.get("/rpc/statistics/count_datasets?to_date=2019-02-13").data + self.assertEqual(res["count"], total_count - after_feb_count) - res = self.client.get('/rpc/statistics/count_datasets?to_date=2019-02-13').data - self.assertEqual(res['count'], total_count - after_feb_count) class StatisticRPCAllDatasetsCumulative(StatisticRPCCommon, CatalogRecordApiWriteCommon): """ Test suite for all_datasets_cumulative. Test only optional parameters removed, legacy and latest for now. """ - url = '/rpc/statistics/all_datasets_cumulative' - dateparam_all = 'from_date=2018-06&to_date=2019-03' + url = "/rpc/statistics/all_datasets_cumulative" + dateparam_all = "from_date=2018-06&to_date=2019-03" def test_all_datasets_cumulative(self): """ @@ -435,50 +442,58 @@ def test_all_datasets_cumulative(self): """ # test bad query parameters - response = self.client.get(f'{self.url}') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'from_date and to_date are required') + response = self.client.get(f"{self.url}") + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + "from_date and to_date are required", + ) - response = self.client.get(f'{self.url}?from_date=2019-11&to_date=bad_parameter') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'date format is YYYY-MM') + response = self.client.get(f"{self.url}?from_date=2019-11&to_date=bad_parameter") + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, "date format is YYYY-MM" + ) - response = self.client.get(f'{self.url}?from_date=2019-11&to_date=2019-11-15') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'date format is YYYY-MM') + response = self.client.get(f"{self.url}?from_date=2019-11&to_date=2019-11-15") + self.assertEqual( + response.status_code, status.HTTP_400_BAD_REQUEST, "date format is YYYY-MM" + ) # test the basic functionality - june_size = self._get_byte_size_of_month('2018-06') - june_count = self._get_dataset_count_of_month('2018-06') + june_size = self._get_byte_size_of_month("2018-06") + june_count = self._get_dataset_count_of_month("2018-06") - july_size = self._get_byte_size_of_month('2018-07') - july_count = self._get_dataset_count_of_month('2018-07') + july_size = self._get_byte_size_of_month("2018-07") + july_count = self._get_dataset_count_of_month("2018-07") - march_size = self._get_byte_size_of_month('2019-03') - march_count = self._get_dataset_count_of_month('2019-03') + march_size = self._get_byte_size_of_month("2019-03") + march_count = self._get_dataset_count_of_month("2019-03") total_count = self._get_total_dataset_count() total_size = self._get_total_byte_size() - response = self.client.get(f'{self.url}?{self.dateparam_all}').data + response = self.client.get(f"{self.url}?{self.dateparam_all}").data # ensure the counts and byte sizes are calculated correctly - self.assertEqual(response[0]['count'], june_count, response) - self.assertEqual(response[0]['ida_byte_size'], june_size, response) - self.assertEqual(response[0]['count_cumulative'], june_count, response) - self.assertEqual(response[0]['ida_byte_size_cumulative'], june_size, response) + self.assertEqual(response[0]["count"], june_count, response) + self.assertEqual(response[0]["ida_byte_size"], june_size, response) + self.assertEqual(response[0]["count_cumulative"], june_count, response) + self.assertEqual(response[0]["ida_byte_size_cumulative"], june_size, response) - self.assertEqual(response[1]['count'], july_count, response) - self.assertEqual(response[1]['ida_byte_size'], july_size, response) - self.assertEqual(response[1]['count_cumulative'], june_count + july_count, response) - self.assertEqual(response[1]['ida_byte_size_cumulative'], june_size + july_size, response) + self.assertEqual(response[1]["count"], july_count, response) + self.assertEqual(response[1]["ida_byte_size"], july_size, response) + self.assertEqual(response[1]["count_cumulative"], june_count + july_count, response) + self.assertEqual(response[1]["ida_byte_size_cumulative"], june_size + july_size, response) - self.assertEqual(response[-1]['count'], march_count, response) - self.assertEqual(response[-1]['ida_byte_size'], march_size, response) - self.assertEqual(response[-1]['count_cumulative'], total_count, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], total_size, response) + self.assertEqual(response[-1]["count"], march_count, response) + self.assertEqual(response[-1]["ida_byte_size"], march_size, response) + self.assertEqual(response[-1]["count_cumulative"], total_count, response) + self.assertEqual(response[-1]["ida_byte_size_cumulative"], total_size, response) # test that only datasets from beginning of from_date is counted - response = self.client.get(f'{self.url}?from_date=2018-07&to_date=2019-03').data + response = self.client.get(f"{self.url}?from_date=2018-07&to_date=2019-03").data - self.assertEqual(response[-1]['count_cumulative'], total_count - june_count, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], total_size - june_size, response) + self.assertEqual(response[-1]["count_cumulative"], total_count - june_count, response) + self.assertEqual(response[-1]["ida_byte_size_cumulative"], total_size - june_size, response) def test_all_datasets_cumulative_single(self): """ @@ -488,92 +503,116 @@ def test_all_datasets_cumulative_single(self): total_size = self._get_total_byte_size() # test removed -parameter - june_size = self._get_byte_size_of_month('2018-06') - june_count = self._get_dataset_count_of_month('2018-06') + june_size = self._get_byte_size_of_month("2018-06") + june_count = self._get_dataset_count_of_month("2018-06") - self._set_removed_dataset(id=8) # belongs to 2018-06, i.e. the first interval + self._set_removed_dataset(id=8) # belongs to 2018-06, i.e. the first interval removed_size = self._get_catalog_record_size(id=8) - response = self.client.get(f'{self.url}?{self.dateparam_all}&removed=true').data + response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=true").data # ensure that only the first month (2018-06) contains dataset and that cumulative is calculated correctly - self.assertEqual(response[0]['count'], 1, response) - self.assertEqual(response[0]['ida_byte_size'], removed_size, response) - self.assertEqual(response[0]['count_cumulative'], 1, response) - self.assertEqual(response[0]['ida_byte_size_cumulative'], removed_size, response) + self.assertEqual(response[0]["count"], 1, response) + self.assertEqual(response[0]["ida_byte_size"], removed_size, response) + self.assertEqual(response[0]["count_cumulative"], 1, response) + self.assertEqual(response[0]["ida_byte_size_cumulative"], removed_size, response) - self.assertEqual(response[-1]['count_cumulative'], 1, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], removed_size, response) + self.assertEqual(response[-1]["count_cumulative"], 1, response) + self.assertEqual(response[-1]["ida_byte_size_cumulative"], removed_size, response) - response = self.client.get(f'{self.url}?{self.dateparam_all}&removed=false').data + response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=false").data # ensure that the correct dataset is missing from results - self.assertEqual(response[0]['count'], june_count - 1, response) - self.assertEqual(response[0]['ida_byte_size'], june_size - removed_size, response) - self.assertEqual(response[-1]['count_cumulative'], total_count - 1, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], total_size - removed_size, response) + self.assertEqual(response[0]["count"], june_count - 1, response) + self.assertEqual(response[0]["ida_byte_size"], june_size - removed_size, response) + self.assertEqual(response[-1]["count_cumulative"], total_count - 1, response) + self.assertEqual( + response[-1]["ida_byte_size_cumulative"], + total_size - removed_size, + response, + ) # test legacy -parameter - leg_cr = self._create_legacy_dataset() # legacy cr belongs to 2019-03, i.e. the last interval - self._set_dataset_creation_date(leg_cr, '2019-03-13') + leg_cr = ( + self._create_legacy_dataset() + ) # legacy cr belongs to 2019-03, i.e. the last interval + self._set_dataset_creation_date(leg_cr, "2019-03-13") legacy_size = self._get_catalog_record_size(id=leg_cr) total_count = self._get_total_dataset_count() total_size = self._get_total_byte_size() - march_size = self._get_byte_size_of_month('2019-03') - march_count = self._get_dataset_count_of_month('2019-03') + march_size = self._get_byte_size_of_month("2019-03") + march_count = self._get_dataset_count_of_month("2019-03") - response = self.client.get(f'{self.url}?{self.dateparam_all}&legacy=true').data + response = self.client.get(f"{self.url}?{self.dateparam_all}&legacy=true").data - self.assertEqual(response[-1]['count'], 1, response) - self.assertEqual(response[-1]['ida_byte_size'], legacy_size, response) - self.assertEqual(response[-1]['count_cumulative'], 1, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], legacy_size, response) + self.assertEqual(response[-1]["count"], 1, response) + self.assertEqual(response[-1]["ida_byte_size"], legacy_size, response) + self.assertEqual(response[-1]["count_cumulative"], 1, response) + self.assertEqual(response[-1]["ida_byte_size_cumulative"], legacy_size, response) - response = self.client.get(f'{self.url}?{self.dateparam_all}&legacy=false').data + response = self.client.get(f"{self.url}?{self.dateparam_all}&legacy=false").data - self.assertEqual(response[-1]['count'], march_count - 1, response) - self.assertEqual(response[-1]['ida_byte_size'], march_size - legacy_size, response) - self.assertEqual(response[-1]['count_cumulative'], total_count - 1, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], total_size - legacy_size, response) + self.assertEqual(response[-1]["count"], march_count - 1, response) + self.assertEqual(response[-1]["ida_byte_size"], march_size - legacy_size, response) + self.assertEqual(response[-1]["count_cumulative"], total_count - 1, response) + self.assertEqual( + response[-1]["ida_byte_size_cumulative"], total_size - legacy_size, response + ) # test latest -parameter # new versions will belong to 2019-03, i.e. the last interval second = self._create_new_dataset_version() - self._set_dataset_creation_date(second, '2019-03-17') + self._set_dataset_creation_date(second, "2019-03-17") old_ver_size = self._get_catalog_record_size(id=1) total_count = self._get_total_dataset_count() total_size = self._get_total_byte_size() - march_size = self._get_byte_size_of_month('2019-03') - march_count = self._get_dataset_count_of_month('2019-03') - - response = self.client.get(f'{self.url}?{self.dateparam_all}&latest=false').data # returns all - self.assertEqual(response[-1]['count'], march_count, response) - self.assertEqual(response[-1]['ida_byte_size'], march_size, response) - self.assertEqual(response[-1]['count_cumulative'], total_count, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], total_size, response) - - with_param = self.client.get(f'{self.url}?{self.dateparam_all}&latest=true').data - self.assertEqual(with_param[-1]['count'], march_count - 1, with_param) - self.assertEqual(with_param[-1]['ida_byte_size'], march_size - old_ver_size, with_param) - self.assertEqual(with_param[-1]['count_cumulative'], total_count - 1, with_param) - self.assertEqual(with_param[-1]['ida_byte_size_cumulative'], total_size - old_ver_size, response) + march_size = self._get_byte_size_of_month("2019-03") + march_count = self._get_dataset_count_of_month("2019-03") + + response = self.client.get( + f"{self.url}?{self.dateparam_all}&latest=false" + ).data # returns all + self.assertEqual(response[-1]["count"], march_count, response) + self.assertEqual(response[-1]["ida_byte_size"], march_size, response) + self.assertEqual(response[-1]["count_cumulative"], total_count, response) + self.assertEqual(response[-1]["ida_byte_size_cumulative"], total_size, response) + + with_param = self.client.get(f"{self.url}?{self.dateparam_all}&latest=true").data + self.assertEqual(with_param[-1]["count"], march_count - 1, with_param) + self.assertEqual(with_param[-1]["ida_byte_size"], march_size - old_ver_size, with_param) + self.assertEqual(with_param[-1]["count_cumulative"], total_count - 1, with_param) + self.assertEqual( + with_param[-1]["ida_byte_size_cumulative"], + total_size - old_ver_size, + response, + ) # ensure that default value(true) is working as expected - without_param = self.client.get(f'{self.url}?{self.dateparam_all}').data - self.assertEqual(with_param[-1]['count'], without_param[-1]['count'], with_param) - self.assertEqual(with_param[-1]['ida_byte_size'], without_param[-1]['ida_byte_size'], with_param) - self.assertEqual(with_param[-1]['count_cumulative'], without_param[-1]['count_cumulative'], with_param) + without_param = self.client.get(f"{self.url}?{self.dateparam_all}").data + self.assertEqual(with_param[-1]["count"], without_param[-1]["count"], with_param) + self.assertEqual( + with_param[-1]["ida_byte_size"], + without_param[-1]["ida_byte_size"], + with_param, + ) self.assertEqual( - with_param[-1]['ida_byte_size_cumulative'], without_param[-1]['ida_byte_size_cumulative'], with_param + with_param[-1]["count_cumulative"], + without_param[-1]["count_cumulative"], + with_param, + ) + self.assertEqual( + with_param[-1]["ida_byte_size_cumulative"], + without_param[-1]["ida_byte_size_cumulative"], + with_param, ) def test_all_datasets_cumulative_removed_latest(self): second = self._create_new_dataset_version() - self._set_dataset_creation_date(second, '2019-03-11') + self._set_dataset_creation_date(second, "2019-03-11") self._set_removed_dataset(id=1) self._set_removed_dataset(id=second) @@ -582,61 +621,81 @@ def test_all_datasets_cumulative_removed_latest(self): removed_size = self._get_catalog_record_size(id=1) + latest_size removed_count = 2 - rem_lat = self.client.get(f'{self.url}?{self.dateparam_all}&removed=true&latest=true').data + rem_lat = self.client.get(f"{self.url}?{self.dateparam_all}&removed=true&latest=true").data - self.assertEqual(rem_lat[-1]['count'], 1, rem_lat) # id=second - self.assertEqual(rem_lat[-1]['ida_byte_size'], latest_size, rem_lat) - self.assertEqual(rem_lat[-1]['count_cumulative'], 1, rem_lat) - self.assertEqual(rem_lat[-1]['ida_byte_size_cumulative'], latest_size, rem_lat) + self.assertEqual(rem_lat[-1]["count"], 1, rem_lat) # id=second + self.assertEqual(rem_lat[-1]["ida_byte_size"], latest_size, rem_lat) + self.assertEqual(rem_lat[-1]["count_cumulative"], 1, rem_lat) + self.assertEqual(rem_lat[-1]["ida_byte_size_cumulative"], latest_size, rem_lat) - rem_not_lat = self.client.get(f'{self.url}?{self.dateparam_all}&removed=true&latest=false').data + rem_not_lat = self.client.get( + f"{self.url}?{self.dateparam_all}&removed=true&latest=false" + ).data - self.assertEqual(rem_not_lat[-1]['count'], removed_count, rem_not_lat) # id=second - self.assertEqual(rem_not_lat[-1]['ida_byte_size'], removed_size, rem_not_lat) - self.assertEqual(rem_not_lat[-1]['count_cumulative'], removed_count, rem_not_lat) - self.assertEqual(rem_not_lat[-1]['ida_byte_size_cumulative'], removed_size, rem_not_lat) + self.assertEqual(rem_not_lat[-1]["count"], removed_count, rem_not_lat) # id=second + self.assertEqual(rem_not_lat[-1]["ida_byte_size"], removed_size, rem_not_lat) + self.assertEqual(rem_not_lat[-1]["count_cumulative"], removed_count, rem_not_lat) + self.assertEqual(rem_not_lat[-1]["ida_byte_size_cumulative"], removed_size, rem_not_lat) # create new dataset with two versions, which will not be deleted new_cr_id = self._create_new_dataset(self.cr_test_data) - self._set_dataset_creation_date(new_cr_id, '2019-01-02') + self._set_dataset_creation_date(new_cr_id, "2019-01-02") new_cr_ver = self._create_new_dataset_version(new_cr_id) - self._set_dataset_creation_date(new_cr_ver, '2019-01-06') + self._set_dataset_creation_date(new_cr_ver, "2019-01-06") old_version_size = self._get_catalog_record_size(id=new_cr_id) - jan_count = self._get_dataset_count_of_month('2019-01') - jan_size = self._get_byte_size_of_month('2019-01') + jan_count = self._get_dataset_count_of_month("2019-01") + jan_size = self._get_byte_size_of_month("2019-01") total_count = self._get_total_dataset_count() total_size = self._get_total_byte_size() - not_rem_lat = self.client.get(f'{self.url}?{self.dateparam_all}&removed=false&latest=true').data + not_rem_lat = self.client.get( + f"{self.url}?{self.dateparam_all}&removed=false&latest=true" + ).data # missing the removed dataset from before and dataset id='new_cr_id' - self.assertEqual(not_rem_lat[-3]['count'], jan_count - 1, not_rem_lat) - self.assertEqual(not_rem_lat[-3]['ida_byte_size'], jan_size - old_version_size, not_rem_lat) - self.assertEqual(not_rem_lat[-1]['count_cumulative'], total_count - removed_count - 1, not_rem_lat) - self.assertEqual(not_rem_lat[-1]['ida_byte_size_cumulative'], total_size - removed_size - old_version_size, - not_rem_lat) + self.assertEqual(not_rem_lat[-3]["count"], jan_count - 1, not_rem_lat) + self.assertEqual(not_rem_lat[-3]["ida_byte_size"], jan_size - old_version_size, not_rem_lat) + self.assertEqual( + not_rem_lat[-1]["count_cumulative"], + total_count - removed_count - 1, + not_rem_lat, + ) + self.assertEqual( + not_rem_lat[-1]["ida_byte_size_cumulative"], + total_size - removed_size - old_version_size, + not_rem_lat, + ) - not_rem_not_lat = self.client.get(f'{self.url}?{self.dateparam_all}&removed=false&latest=false').data + not_rem_not_lat = self.client.get( + f"{self.url}?{self.dateparam_all}&removed=false&latest=false" + ).data - self.assertEqual(not_rem_not_lat[-3]['count'], jan_count, not_rem_not_lat) - self.assertEqual(not_rem_not_lat[-3]['ida_byte_size'], jan_size, not_rem_not_lat) - self.assertEqual(not_rem_not_lat[-1]['count_cumulative'], total_count - removed_count, - not_rem_not_lat) - self.assertEqual(not_rem_not_lat[-1]['ida_byte_size_cumulative'], total_size - removed_size, not_rem_not_lat) + self.assertEqual(not_rem_not_lat[-3]["count"], jan_count, not_rem_not_lat) + self.assertEqual(not_rem_not_lat[-3]["ida_byte_size"], jan_size, not_rem_not_lat) + self.assertEqual( + not_rem_not_lat[-1]["count_cumulative"], + total_count - removed_count, + not_rem_not_lat, + ) + self.assertEqual( + not_rem_not_lat[-1]["ida_byte_size_cumulative"], + total_size - removed_size, + not_rem_not_lat, + ) def test_all_datasets_cumulative_removed_legacy(self): leg_cr_1 = self._create_legacy_dataset() - self._set_dataset_creation_date(leg_cr_1, '2018-07-03') + self._set_dataset_creation_date(leg_cr_1, "2018-07-03") leg_cr_2 = self._create_legacy_dataset() - self._set_dataset_creation_date(leg_cr_2, '2019-02-08') + self._set_dataset_creation_date(leg_cr_2, "2019-02-08") self._set_removed_dataset(leg_cr_2) - self._set_removed_dataset(id=8) # belongs to first interval, i.e. 2018-06 + self._set_removed_dataset(id=8) # belongs to first interval, i.e. 2018-06 leg_non_rem_size = self._get_catalog_record_size(leg_cr_1) leg_removed_size = self._get_catalog_record_size(leg_cr_2) @@ -648,48 +707,70 @@ def test_all_datasets_cumulative_removed_legacy(self): total_count = self._get_total_dataset_count() total_size = self._get_total_byte_size() - june_count = self._get_dataset_count_of_month('2018-06') - june_size = self._get_byte_size_of_month('2018-06') + june_count = self._get_dataset_count_of_month("2018-06") + june_size = self._get_byte_size_of_month("2018-06") - feb_count = self._get_dataset_count_of_month('2019-02') - feb_size = self._get_byte_size_of_month('2019-02') + feb_count = self._get_dataset_count_of_month("2019-02") + feb_size = self._get_byte_size_of_month("2019-02") - rem_leg = self.client.get(f'{self.url}?{self.dateparam_all}&removed=true&legacy=true').data + rem_leg = self.client.get(f"{self.url}?{self.dateparam_all}&removed=true&legacy=true").data - self.assertEqual(rem_leg[-2]['count'], 1, rem_leg) - self.assertEqual(rem_leg[-2]['ida_byte_size'], leg_removed_size, rem_leg) - self.assertEqual(rem_leg[-1]['count_cumulative'], 1, rem_leg) - self.assertEqual(rem_leg[-1]['ida_byte_size_cumulative'], leg_removed_size, rem_leg) + self.assertEqual(rem_leg[-2]["count"], 1, rem_leg) + self.assertEqual(rem_leg[-2]["ida_byte_size"], leg_removed_size, rem_leg) + self.assertEqual(rem_leg[-1]["count_cumulative"], 1, rem_leg) + self.assertEqual(rem_leg[-1]["ida_byte_size_cumulative"], leg_removed_size, rem_leg) - rem_not_leg = self.client.get(f'{self.url}?{self.dateparam_all}&removed=true&legacy=false').data + rem_not_leg = self.client.get( + f"{self.url}?{self.dateparam_all}&removed=true&legacy=false" + ).data - self.assertEqual(rem_not_leg[0]['count'], 1, rem_not_leg) - self.assertEqual(rem_not_leg[0]['ida_byte_size'], removed_size, rem_not_leg) - self.assertEqual(rem_not_leg[-1]['count_cumulative'], 1, rem_not_leg) - self.assertEqual(rem_not_leg[-1]['ida_byte_size_cumulative'], removed_size, rem_not_leg) + self.assertEqual(rem_not_leg[0]["count"], 1, rem_not_leg) + self.assertEqual(rem_not_leg[0]["ida_byte_size"], removed_size, rem_not_leg) + self.assertEqual(rem_not_leg[-1]["count_cumulative"], 1, rem_not_leg) + self.assertEqual(rem_not_leg[-1]["ida_byte_size_cumulative"], removed_size, rem_not_leg) - not_rem_leg = self.client.get(f'{self.url}?{self.dateparam_all}&removed=false&legacy=true').data + not_rem_leg = self.client.get( + f"{self.url}?{self.dateparam_all}&removed=false&legacy=true" + ).data - self.assertEqual(not_rem_leg[1]['count'], 1, not_rem_leg) - self.assertEqual(not_rem_leg[1]['ida_byte_size'], leg_non_rem_size, not_rem_leg) - self.assertEqual(not_rem_leg[-1]['count_cumulative'], 1, not_rem_leg) - self.assertEqual(not_rem_leg[-1]['ida_byte_size_cumulative'], leg_non_rem_size, not_rem_leg) + self.assertEqual(not_rem_leg[1]["count"], 1, not_rem_leg) + self.assertEqual(not_rem_leg[1]["ida_byte_size"], leg_non_rem_size, not_rem_leg) + self.assertEqual(not_rem_leg[-1]["count_cumulative"], 1, not_rem_leg) + self.assertEqual(not_rem_leg[-1]["ida_byte_size_cumulative"], leg_non_rem_size, not_rem_leg) - not_rem_not_leg = self.client.get(f'{self.url}?{self.dateparam_all}&removed=false&legacy=false').data + not_rem_not_leg = self.client.get( + f"{self.url}?{self.dateparam_all}&removed=false&legacy=false" + ).data - self.assertEqual(not_rem_not_leg[0]['count'], june_count - 1, not_rem_not_leg) - self.assertEqual(not_rem_not_leg[0]['ida_byte_size'], june_size - removed_size, not_rem_not_leg) - self.assertEqual(not_rem_not_leg[-2]['count'], feb_count - 1, not_rem_not_leg) - self.assertEqual(not_rem_not_leg[-2]['ida_byte_size'], feb_size - leg_removed_size, not_rem_not_leg) - self.assertEqual(not_rem_not_leg[-1]['count_cumulative'], total_count - rem_leg_count, not_rem_not_leg) - self.assertEqual(not_rem_not_leg[-1]['ida_byte_size_cumulative'], total_size - rem_leg_size, not_rem_not_leg) + self.assertEqual(not_rem_not_leg[0]["count"], june_count - 1, not_rem_not_leg) + self.assertEqual( + not_rem_not_leg[0]["ida_byte_size"], + june_size - removed_size, + not_rem_not_leg, + ) + self.assertEqual(not_rem_not_leg[-2]["count"], feb_count - 1, not_rem_not_leg) + self.assertEqual( + not_rem_not_leg[-2]["ida_byte_size"], + feb_size - leg_removed_size, + not_rem_not_leg, + ) + self.assertEqual( + not_rem_not_leg[-1]["count_cumulative"], + total_count - rem_leg_count, + not_rem_not_leg, + ) + self.assertEqual( + not_rem_not_leg[-1]["ida_byte_size_cumulative"], + total_size - rem_leg_size, + not_rem_not_leg, + ) def test_all_datasets_cumulative_latest_legacy(self): leg_cr = self._create_legacy_dataset() - self._set_dataset_creation_date(leg_cr, '2019-03-08') + self._set_dataset_creation_date(leg_cr, "2019-03-08") second = self._create_new_dataset_version(leg_cr) - self._set_dataset_creation_date(second, '2019-03-12') + self._set_dataset_creation_date(second, "2019-03-12") leg_cr_size = self._get_catalog_record_size(id=leg_cr) second_size = self._get_catalog_record_size(id=second) @@ -700,113 +781,154 @@ def test_all_datasets_cumulative_latest_legacy(self): total_count = self._get_total_dataset_count() total_size = self._get_total_byte_size() - march_count = self._get_dataset_count_of_month('2019-03') - march_size = self._get_byte_size_of_month('2019-03') + march_count = self._get_dataset_count_of_month("2019-03") + march_size = self._get_byte_size_of_month("2019-03") - leg_lat = self.client.get(f'{self.url}?{self.dateparam_all}&legacy=true&latest=true').data + leg_lat = self.client.get(f"{self.url}?{self.dateparam_all}&legacy=true&latest=true").data - self.assertEqual(leg_lat[-1]['count'], 1, leg_lat) - self.assertEqual(leg_lat[-1]['ida_byte_size'], second_size, leg_lat) - self.assertEqual(leg_lat[-1]['count_cumulative'], 1, leg_lat) - self.assertEqual(leg_lat[-1]['ida_byte_size_cumulative'], second_size, leg_lat) + self.assertEqual(leg_lat[-1]["count"], 1, leg_lat) + self.assertEqual(leg_lat[-1]["ida_byte_size"], second_size, leg_lat) + self.assertEqual(leg_lat[-1]["count_cumulative"], 1, leg_lat) + self.assertEqual(leg_lat[-1]["ida_byte_size_cumulative"], second_size, leg_lat) - leg_not_lat = self.client.get(f'{self.url}?{self.dateparam_all}&legacy=true&latest=false').data + leg_not_lat = self.client.get( + f"{self.url}?{self.dateparam_all}&legacy=true&latest=false" + ).data - self.assertEqual(leg_not_lat[-1]['count'], legacy_count, leg_not_lat) - self.assertEqual(leg_not_lat[-1]['ida_byte_size'], legacy_size, leg_not_lat) - self.assertEqual(leg_not_lat[-1]['count_cumulative'], legacy_count, leg_not_lat) - self.assertEqual(leg_not_lat[-1]['ida_byte_size_cumulative'], legacy_size, leg_not_lat) + self.assertEqual(leg_not_lat[-1]["count"], legacy_count, leg_not_lat) + self.assertEqual(leg_not_lat[-1]["ida_byte_size"], legacy_size, leg_not_lat) + self.assertEqual(leg_not_lat[-1]["count_cumulative"], legacy_count, leg_not_lat) + self.assertEqual(leg_not_lat[-1]["ida_byte_size_cumulative"], legacy_size, leg_not_lat) - not_leg_not_lat = self.client.get(f'{self.url}?{self.dateparam_all}&legacy=false&latest=false').data + not_leg_not_lat = self.client.get( + f"{self.url}?{self.dateparam_all}&legacy=false&latest=false" + ).data - self.assertEqual(not_leg_not_lat[-1]['count'], march_count - legacy_count, not_leg_not_lat) - self.assertEqual(not_leg_not_lat[-1]['ida_byte_size'], march_size - legacy_size, not_leg_not_lat) - self.assertEqual(not_leg_not_lat[-1]['count_cumulative'], total_count - legacy_count, not_leg_not_lat) - self.assertEqual(not_leg_not_lat[-1]['ida_byte_size_cumulative'], total_size - legacy_size, not_leg_not_lat) + self.assertEqual(not_leg_not_lat[-1]["count"], march_count - legacy_count, not_leg_not_lat) + self.assertEqual( + not_leg_not_lat[-1]["ida_byte_size"], + march_size - legacy_size, + not_leg_not_lat, + ) + self.assertEqual( + not_leg_not_lat[-1]["count_cumulative"], + total_count - legacy_count, + not_leg_not_lat, + ) + self.assertEqual( + not_leg_not_lat[-1]["ida_byte_size_cumulative"], + total_size - legacy_size, + not_leg_not_lat, + ) class StatisticRPCforDrafts(StatisticRPCCommon, CatalogRecordApiWriteCommon): """ Tests that drafts are not taken into account when calculating statistics """ + def test_count_datasets_api_for_drafts(self): """ Tests that rpc/statistics/count_datasets returns only count of published datasets """ - response_1 = self.client.get('/rpc/statistics/count_datasets').data + response_1 = self.client.get("/rpc/statistics/count_datasets").data self._set_dataset_as_draft(1) - self.assertEqual(CatalogRecord.objects.get(pk=1).state, 'draft', - 'Dataset with id=1 should have changed state to draft') + self.assertEqual( + CatalogRecord.objects.get(pk=1).state, + "draft", + "Dataset with id=1 should have changed state to draft", + ) - response_2 = self.client.get('/rpc/statistics/count_datasets').data - self.assertNotEqual(response_1['count'], response_2['count'], - 'Drafts should not be returned in count_datasets api') + response_2 = self.client.get("/rpc/statistics/count_datasets").data + self.assertNotEqual( + response_1["count"], + response_2["count"], + "Drafts should not be returned in count_datasets api", + ) def test_all_datasets_cumulative_for_drafts(self): """ Tests that /rpc/statistics/all_datasets_cumulative returns only published datasets """ - url = '/rpc/statistics/all_datasets_cumulative?from_date=2019-06&to_date=2019-06' + url = "/rpc/statistics/all_datasets_cumulative?from_date=2019-06&to_date=2019-06" - self._set_dataset_creation_date(1, '2019-06-15') + self._set_dataset_creation_date(1, "2019-06-15") response_1 = self.client.get(url).data self._set_dataset_as_draft(1) response_2 = self.client.get(url).data # ensure the counts and byte sizes are calculated without drafts - self.assertNotEqual(response_1[0]['count'], response_2[0]['count'], - 'Count for June should reduce by one as dataset id=1 was set as draft') - self.assertNotEqual(response_1[0]['ida_byte_size'], response_2[0]['ida_byte_size'], - 'Byte size for June should reduce by one as dataset id=1 was set as draft') + self.assertNotEqual( + response_1[0]["count"], + response_2[0]["count"], + "Count for June should reduce by one as dataset id=1 was set as draft", + ) + self.assertNotEqual( + response_1[0]["ida_byte_size"], + response_2[0]["ida_byte_size"], + "Byte size for June should reduce by one as dataset id=1 was set as draft", + ) def test_catalog_datasets_cumulative_for_drafts(self): """ Tests that /rpc/statistics/catalog_datasets_cumulative returns only published datasets """ - url = '/rpc/statistics/catalog_datasets_cumulative?from_date=2019-06-01&to_date=2019-06-30' + url = "/rpc/statistics/catalog_datasets_cumulative?from_date=2019-06-01&to_date=2019-06-30" catalog = "urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d3" - self._set_dataset_creation_date(1, '2019-06-15') - self._set_cr_datacatalog(1, catalog) # Adds id=1 to catalog + self._set_dataset_creation_date(1, "2019-06-15") + self._set_cr_datacatalog(1, catalog) # Adds id=1 to catalog - count_1 = self.client.get(url).data[catalog]['open'][0]['count'] - total_1 = self.client.get(url).data[catalog]['total'] + count_1 = self.client.get(url).data[catalog]["open"][0]["count"] + total_1 = self.client.get(url).data[catalog]["total"] self._set_dataset_as_draft(1) - count_2 = self.client.get(url).data[catalog]['open'][0]['count'] - total_2 = self.client.get(url).data[catalog]['total'] + count_2 = self.client.get(url).data[catalog]["open"][0]["count"] + total_2 = self.client.get(url).data[catalog]["total"] # ensure the count and total are calculated without drafts - self.assertNotEqual(count_1, count_2, 'Count should reduce by one as dataset id=1 was set as draft') - self.assertNotEqual(total_1, total_2, 'Total should reduce by one as dataset id=1 was set as draft') + self.assertNotEqual( + count_1, + count_2, + "Count should reduce by one as dataset id=1 was set as draft", + ) + self.assertNotEqual( + total_1, + total_2, + "Total should reduce by one as dataset id=1 was set as draft", + ) def test_end_user_datasets_cumulative_for_drafts(self): - ''' End user api should return only published data ''' - url = '/rpc/statistics/end_user_datasets_cumulative?from_date=2019-06-01&to_date=2019-06-30' + """ End user api should return only published data """ + url = "/rpc/statistics/end_user_datasets_cumulative?from_date=2019-06-01&to_date=2019-06-30" - self._set_dataset_creation_date(10, '2019-06-15') - count_1 = self.client.get(url).data[0]['count'] + self._set_dataset_creation_date(10, "2019-06-15") + count_1 = self.client.get(url).data[0]["count"] self._set_dataset_as_draft(10) - count_2 = self.client.get(url).data[0]['count'] + count_2 = self.client.get(url).data[0]["count"] # ensure the count are calculated without drafts - self.assertNotEqual(count_1, count_2, 'Count should be reduced by one after setting id=10 as draft') + self.assertNotEqual( + count_1, + count_2, + "Count should be reduced by one after setting id=10 as draft", + ) def test_organization_datasets_cumulative_for_drafts(self): - ''' Organization api should return only published data ''' + """ Organization api should return only published data """ url = "/rpc/statistics/organization_datasets_cumulative?from_date=2019-06-01&to_date=2019-06-30" - self._set_dataset_creation_date(1, '2019-06-15') - self._set_cr_organization(1, 'org_2') - total_1 = self.client.get(url).data['org_2']['total'] + self._set_dataset_creation_date(1, "2019-06-15") + self._set_cr_organization(1, "org_2") + total_1 = self.client.get(url).data["org_2"]["total"] self._set_dataset_as_draft(1) - total_2 = self.client.get(url).data['org_2']['total'] + total_2 = self.client.get(url).data["org_2"]["total"] # ensure the totals are calculated without drafts - self.assertNotEqual(total_1, total_2, 'Count be reduced by one after setting id=1 as draft') \ No newline at end of file + self.assertNotEqual(total_1, total_2, "Count be reduced by one after setting id=1 as draft") diff --git a/src/metax_api/tests/api/rpc/v2/views/__init__.py b/src/metax_api/tests/api/rpc/v2/views/__init__.py index 1fe254c9..e965fca2 100755 --- a/src/metax_api/tests/api/rpc/v2/views/__init__.py +++ b/src/metax_api/tests/api/rpc/v2/views/__init__.py @@ -5,7 +5,6 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from .common_rpc import * from .dataset_rpc import * from .file_rpc import * from .statistic_rpc import * diff --git a/src/metax_api/tests/api/rpc/v2/views/common_rpc.py b/src/metax_api/tests/api/rpc/v2/views/common_rpc.py deleted file mode 100755 index be010ab5..00000000 --- a/src/metax_api/tests/api/rpc/v2/views/common_rpc.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from rest_framework import status -from rest_framework.test import APITestCase - -from metax_api.tests.utils import TestClassUtils - - -class CommonRPCTests(APITestCase, TestClassUtils): - - def test_list_valid_methods(self): - """ - When an invalid (or mistyped) method name is attempted, the api should list valid methods - names for that RPC endpoint. - """ - response = self.client.get('/rpc/v2/datasets/nonexisting') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual('methods are: ' in response.data['detail'][0], True, response.content) diff --git a/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py b/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py index b85a949c..d89f0c29 100755 --- a/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py +++ b/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py @@ -7,116 +7,15 @@ import responses from django.conf import settings -from django.core.management import call_command from rest_framework import status -from rest_framework.test import APITestCase from metax_api.models import CatalogRecordV2 from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon -from metax_api.tests.utils import TestClassUtils, get_test_oidc_token, test_data_file_path +from metax_api.tests.utils import get_test_oidc_token CR = CatalogRecordV2 -class DatasetRPCTests(APITestCase, TestClassUtils): - - @classmethod - def setUpClass(cls): - """ - Loaded only once for test cases inside this class. - """ - super().setUpClass() - call_command('loaddata', test_data_file_path, verbosity=0) - - def setUp(self): - super().setUp() - self.create_end_user_data_catalogs() - - @responses.activate - def test_get_minimal_dataset_template(self): - """ - Retrieve and use a minimal dataset template example from the api. - """ - - # query param type is missing, should return error and description what to do. - response = self.client.get('/rpc/v2/datasets/get_minimal_dataset_template') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # test preventing typos - response = self.client.get('/rpc/v2/datasets/get_minimal_dataset_template?type=wrong') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # test minimal dataset for service use - response = self.client.get('/rpc/v2/datasets/get_minimal_dataset_template?type=service') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue('metadata_provider_org' in response.data) - self.assertTrue('metadata_provider_user' in response.data) - self._use_http_authorization(username='testuser') - response = self.client.post('/rest/v2/datasets', response.data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - # test minimal dataset for end user use - response = self.client.get('/rpc/v2/datasets/get_minimal_dataset_template?type=enduser') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue('metadata_provider_org' not in response.data) - self.assertTrue('metadata_provider_user' not in response.data) - self._use_http_authorization(method='bearer', token=get_test_oidc_token()) - self._mock_token_validation_succeeds() - response = self.client.post('/rest/v2/datasets', response.data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - def test_set_preservation_identifier(self): - self._set_http_authorization('service') - - # Parameter 'identifier' is required - response = self.client.post('/rpc/v2/datasets/set_preservation_identifier') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # Nonexisting identifier should return 404 - response = self.client.post('/rpc/v2/datasets/set_preservation_identifier?identifier=nonexisting') - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - # Create ida data catalog - dc = self._get_object_from_test_data('datacatalog', requested_index=0) - dc_id = settings.IDA_DATA_CATALOG_IDENTIFIER - dc['catalog_json']['identifier'] = dc_id - self.client.post('/rest/v2/datacatalogs', dc, format="json") - - # Test OK ops - - # Create new ida cr without doi - cr_json = self.client.get('/rest/v2/datasets/1').data - cr_json.pop('preservation_identifier', None) - cr_json.pop('identifier') - cr_json['research_dataset'].pop('preferred_identifier', None) - cr_json['data_catalog'] = dc_id - cr_json['research_dataset']['issued'] = '2018-01-01' - cr_json['research_dataset']['publisher'] = { - '@type': 'Organization', - 'name': { 'en': 'publisher' } - } - - response = self.client.post('/rest/v2/datasets?pid_type=urn', cr_json, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - identifier = response.data['identifier'] - - # Verify rpc api returns the same doi as the one that is set to the datasets' preservation identifier - response = self.client.post(f'/rpc/v2/datasets/set_preservation_identifier?identifier={identifier}') - self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - - response2 = self.client.get(f'/rest/v2/datasets/{identifier}') - self.assertEqual(response2.status_code, status.HTTP_200_OK, response2.data) - self.assertEqual(response.data, response2.data['preservation_identifier'], response2.data) - - # Return 400 if request is not correct datacite format - response2.data['research_dataset'].pop('issued') - response = self.client.put(f'/rest/v2/datasets/{identifier}', response2.data, format="json") - self.assertEqual(response2.status_code, status.HTTP_200_OK, response2.data) - - response = self.client.post(f'/rpc/v2/datasets/set_preservation_identifier?identifier={identifier}') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - - class ChangeCumulativeStateRPC(CatalogRecordApiWriteCommon): """ @@ -125,40 +24,49 @@ class ChangeCumulativeStateRPC(CatalogRecordApiWriteCommon): """ def _create_cumulative_dataset(self, state): - self.cr_test_data['cumulative_state'] = state + self.cr_test_data["cumulative_state"] = state - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self.assertEqual(response.data['cumulative_state'], state, response.data) + self.assertEqual(response.data["cumulative_state"], state, response.data) return response.data def _update_cr_cumulative_state(self, identifier, state, result=status.HTTP_204_NO_CONTENT): response = self.client.post( - '/rpc/v2/datasets/change_cumulative_state?identifier=%s&cumulative_state=%d' % (identifier, state), - format="json" + "/rpc/v2/datasets/change_cumulative_state?identifier=%s&cumulative_state=%d" + % (identifier, state), + format="json", ) self.assertEqual(response.status_code, result, response.data) return response.data def _get_cr(self, identifier): - response = self.client.get('/rest/v2/datasets/%s' % identifier, format="json") + response = self.client.get("/rest/v2/datasets/%s" % identifier, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) return response.data def _assert_file_counts(self, new_version): - new_count = CatalogRecordV2.objects.get(pk=new_version['id']).files.count() - old_count = CatalogRecordV2.objects.get(pk=new_version['previous_dataset_version']['id']).files.count() - self.assertEqual(new_count, old_count, 'file count between versions should match') + new_count = CatalogRecordV2.objects.get(pk=new_version["id"]).files.count() + old_count = CatalogRecordV2.objects.get( + pk=new_version["previous_dataset_version"]["id"] + ).files.count() + self.assertEqual(new_count, old_count, "file count between versions should match") def test_transitions_from_NO(self): """ If dataset is published, and it has files, state can't be changed from NO to anything. """ cr_orig = self._create_cumulative_dataset(0) - self._update_cr_cumulative_state(cr_orig['identifier'], CR.CUMULATIVE_STATE_YES, status.HTTP_400_BAD_REQUEST) - self._update_cr_cumulative_state(cr_orig['identifier'], CR.CUMULATIVE_STATE_CLOSED, status.HTTP_400_BAD_REQUEST) + self._update_cr_cumulative_state( + cr_orig["identifier"], CR.CUMULATIVE_STATE_YES, status.HTTP_400_BAD_REQUEST + ) + self._update_cr_cumulative_state( + cr_orig["identifier"], + CR.CUMULATIVE_STATE_CLOSED, + status.HTTP_400_BAD_REQUEST, + ) def test_transitions_from_YES(self): """ @@ -166,13 +74,20 @@ def test_transitions_from_YES(self): """ cr = self._create_cumulative_dataset(1) orig_record_count = CatalogRecordV2.objects.all().count() - self._update_cr_cumulative_state(cr['identifier'], CR.CUMULATIVE_STATE_NO, status.HTTP_400_BAD_REQUEST) + self._update_cr_cumulative_state( + cr["identifier"], CR.CUMULATIVE_STATE_NO, status.HTTP_400_BAD_REQUEST + ) self.assertEqual(CatalogRecordV2.objects.all().count(), orig_record_count) # active to non-active cumulation is legal - self._update_cr_cumulative_state(cr['identifier'], CR.CUMULATIVE_STATE_CLOSED) - cr = self._get_cr(cr['identifier']) - self.assertEqual(cr['cumulative_state'], CR.CUMULATIVE_STATE_CLOSED, 'dataset should have changed status') + self._update_cr_cumulative_state(cr["identifier"], CR.CUMULATIVE_STATE_CLOSED) + cr = self._get_cr(cr["identifier"]) + self.assertEqual( + cr["cumulative_state"], + CR.CUMULATIVE_STATE_CLOSED, + "dataset should have changed status", + ) + class CatalogRecordVersionHandling(CatalogRecordApiWriteCommon): @@ -185,49 +100,67 @@ def test_create_new_version(self): """ A new dataset version can be created for datasets in data catalogs that support versioning. """ - response = self.client.post('/rpc/v2/datasets/create_new_version?identifier=1', format="json") + response = self.client.post( + "/rpc/v2/datasets/create_new_version?identifier=1", format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - next_version_identifier = response.data.get('identifier') + next_version_identifier = response.data.get("identifier") - response = self.client.get('/rest/v2/datasets/1', format="json") + response = self.client.get("/rest/v2/datasets/1", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data.get('next_dataset_version', {}).get('identifier'), next_version_identifier) + self.assertEqual( + response.data.get("next_dataset_version", {}).get("identifier"), + next_version_identifier, + ) - response2 = self.client.get('/rest/v2/datasets/%s' % next_version_identifier, format="json") + response2 = self.client.get("/rest/v2/datasets/%s" % next_version_identifier, format="json") self.assertEqual(response2.status_code, status.HTTP_200_OK, response2.data) self.assertEqual( - response2.data.get('previous_dataset_version', {}).get('identifier'), response.data['identifier'] + response2.data.get("previous_dataset_version", {}).get("identifier"), + response.data["identifier"], ) def test_delete_new_version_draft(self): """ Ensure a new version that is created into draft state can be deleted, and is permanently deleted. """ - response = self.client.post('/rpc/v2/datasets/create_new_version?identifier=1', format="json") + response = self.client.post( + "/rpc/v2/datasets/create_new_version?identifier=1", format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - next_version_identifier = response.data.get('identifier') + next_version_identifier = response.data.get("identifier") - response = self.client.delete('/rest/v2/datasets/%s' % next_version_identifier, format="json") + response = self.client.delete( + "/rest/v2/datasets/%s" % next_version_identifier, format="json" + ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) - num_found = CatalogRecordV2.objects_unfiltered.filter(identifier=next_version_identifier).count() - self.assertEqual(num_found, 0, 'draft should have been permanently deleted') + num_found = CatalogRecordV2.objects_unfiltered.filter( + identifier=next_version_identifier + ).count() + self.assertEqual(num_found, 0, "draft should have been permanently deleted") self.assertEqual(CatalogRecordV2.objects.get(pk=1).next_dataset_version, None) def test_version_already_exists(self): """ If a dataset already has a next version, then a new version cannot be created. """ - response = self.client.post('/rpc/v2/datasets/create_new_version?identifier=1', format="json") + response = self.client.post( + "/rpc/v2/datasets/create_new_version?identifier=1", format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) response = self.client.post( - '/rpc/v2/datasets/create_new_version?identifier=1', format="json" + "/rpc/v2/datasets/create_new_version?identifier=1", format="json" ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertEqual('already has a next version' in response.data['detail'][0], True, response.data) + self.assertEqual( + "already has a next version" in response.data["detail"][0], + True, + response.data, + ) def test_new_version_removes_deprecated_files(self): """ @@ -236,40 +169,48 @@ def test_new_version_removes_deprecated_files(self): """ original_cr = CatalogRecordV2.objects.get(pk=1) - response = self.client.delete('/rest/v2/files/1', format="json") + response = self.client.delete("/rest/v2/files/1", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.post('/rpc/v2/datasets/create_new_version?identifier=1', format="json") + response = self.client.post( + "/rpc/v2/datasets/create_new_version?identifier=1", format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - new_cr = CatalogRecordV2.objects.get(pk=response.data['id']) + new_cr = CatalogRecordV2.objects.get(pk=response.data["id"]) self.assertEqual(new_cr.deprecated, False) - self.assertTrue(len(new_cr.research_dataset['files']) < len(original_cr.research_dataset['files'])) - self.assertTrue(new_cr.files.count() < original_cr.files(manager='objects_unfiltered').count()) + self.assertTrue( + len(new_cr.research_dataset["files"]) < len(original_cr.research_dataset["files"]) + ) + self.assertTrue( + new_cr.files.count() < original_cr.files(manager="objects_unfiltered").count() + ) def test_version_from_draft(self): """ New versions cannot be created from drafts """ - response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets?draft", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - dft_id = response.data['identifier'] + dft_id = response.data["identifier"] - response = self.client.post(f'/rpc/v2/datasets/create_new_version?identifier={dft_id}', format="json") + response = self.client.post( + f"/rpc/v2/datasets/create_new_version?identifier={dft_id}", format="json" + ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('draft' in response.data['detail'][0], response.data) + self.assertTrue("draft" in response.data["detail"][0], response.data) def test_draft_blocks_version_creation(self): """ Don't allow new versions if there are unmerged drafts for a dataset """ - response = self.client.post('/rpc/v2/datasets/create_draft?identifier=1') + response = self.client.post("/rpc/v2/datasets/create_draft?identifier=1") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - response = self.client.post('/rpc/v2/datasets/create_new_version?identifier=1') + response = self.client.post("/rpc/v2/datasets/create_new_version?identifier=1") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) - self.assertTrue('unmerged draft' in response.data['detail'][0], response.data) + self.assertTrue("unmerged draft" in response.data["detail"][0], response.data) @responses.activate def test_authorization(self): @@ -278,45 +219,53 @@ def test_authorization(self): """ # service use should be OK - response = self.client.post('/rpc/v2/datasets/create_new_version?identifier=2', format="json") + response = self.client.post( + "/rpc/v2/datasets/create_new_version?identifier=2", format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) # test with end user, should fail self.token = get_test_oidc_token(new_proxy=True) self._mock_token_validation_succeeds() - self._use_http_authorization(method='bearer', token=self.token) + self._use_http_authorization(method="bearer", token=self.token) - response = self.client.post('/rpc/v2/datasets/create_new_version?identifier=1', format="json") + response = self.client.post( + "/rpc/v2/datasets/create_new_version?identifier=1", format="json" + ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) # change owner, try again. should be OK cr = CatalogRecordV2.objects.get(pk=1) - cr.metadata_provider_user = self.token['CSCUserName'] - cr.editor = None + cr.metadata_provider_user = self.token["CSCUserName"] cr.force_save() - response = self.client.post('/rpc/v2/datasets/create_new_version?identifier=1', format="json") + response = self.client.post( + "/rpc/v2/datasets/create_new_version?identifier=1", format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) class CatalogRecordPublishing(CatalogRecordApiWriteCommon): - def test_publish_new_dataset_draft(self): - response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets?draft", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - cr_id = response.data['id'] + cr_id = response.data["id"] - response = self.client.post('/rpc/v2/datasets/publish_dataset?identifier=%d' % cr_id, format="json") + response = self.client.post( + "/rpc/v2/datasets/publish_dataset?identifier=%d" % cr_id, format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertTrue(response.data.get('preferred_identifier') is not None) + self.assertTrue(response.data.get("preferred_identifier") is not None) - response = self.client.get('/rest/v2/datasets/%d' % cr_id, format="json") + response = self.client.get("/rest/v2/datasets/%d" % cr_id, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - self.assertEqual(response.data['state'], 'published') + self.assertEqual(response.data["state"], "published") self.assertEqual( - response.data['research_dataset']['preferred_identifier'] == response.data['identifier'], False + response.data["research_dataset"]["preferred_identifier"] + == response.data["identifier"], + False, ) @responses.activate @@ -328,36 +277,37 @@ def test_authorization(self): """ # test with service - response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets?draft", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) response = self.client.post( - '/rpc/v2/datasets/publish_dataset?identifier=%d' % response.data['id'], format="json" + "/rpc/v2/datasets/publish_dataset?identifier=%d" % response.data["id"], + format="json", ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # test with end user self.token = get_test_oidc_token(new_proxy=True) self._mock_token_validation_succeeds() - self._use_http_authorization(method='bearer', token=self.token) + self._use_http_authorization(method="bearer", token=self.token) self.create_end_user_data_catalogs() - self.cr_test_data['data_catalog'] = settings.END_USER_ALLOWED_DATA_CATALOGS[0] - self.cr_test_data['research_dataset'].pop('files', None) - self.cr_test_data['research_dataset'].pop('directories', None) + self.cr_test_data["data_catalog"] = settings.END_USER_ALLOWED_DATA_CATALOGS[0] + self.cr_test_data["research_dataset"].pop("files", None) + self.cr_test_data["research_dataset"].pop("directories", None) - response = self.client.post('/rest/v2/datasets?draft', self.cr_test_data, format="json") + response = self.client.post("/rest/v2/datasets?draft", self.cr_test_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) response = self.client.post( - '/rpc/v2/datasets/publish_dataset?identifier=%d' % response.data['id'], format="json" + "/rpc/v2/datasets/publish_dataset?identifier=%d" % response.data["id"], + format="json", ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) class CatalogRecordV1APIs(CatalogRecordApiWriteCommon): - def test_ensure_v1_apis_dont_work(self): - for endpoint in ('fix_deprecated', 'refresh_directory_content'): - response = self.client.post(f'/rpc/v2/datasets/{endpoint}', format="json") + for endpoint in ("fix_deprecated", "refresh_directory_content"): + response = self.client.post(f"/rpc/v2/datasets/{endpoint}", format="json") self.assertEqual(response.status_code, 501, response.data) diff --git a/src/metax_api/tests/api/rpc/v2/views/file_rpc.py b/src/metax_api/tests/api/rpc/v2/views/file_rpc.py index fce29e46..8d8d6828 100755 --- a/src/metax_api/tests/api/rpc/v2/views/file_rpc.py +++ b/src/metax_api/tests/api/rpc/v2/views/file_rpc.py @@ -9,20 +9,20 @@ from rest_framework import status from rest_framework.test import APITestCase -from metax_api.models import CatalogRecordV2, Directory, File +from metax_api.models import CatalogRecordV2, File from metax_api.tests.utils import TestClassUtils, test_data_file_path class FileRPCTests(APITestCase, TestClassUtils): - def setUp(self): """ Reloaded for every test case """ super().setUp() - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) self._use_http_authorization() + class DeleteProjectTests(FileRPCTests): """ @@ -30,44 +30,12 @@ class DeleteProjectTests(FileRPCTests): correct result for successful operations. """ - def test_wrong_parameters(self): - # correct user, no project identifier - response = self.client.post('/rpc/v2/files/delete_project') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # nonexisting project identifier: - response = self.client.post('/rpc/v2/files/delete_project?project_identifier=non_existing') - self.assertEqual(response.data['deleted_files_count'], 0) - - # wrong request method - response = self.client.delete('/rpc/v2/files/delete_project?project_identifier=research_project_112') - self.assertEqual(response.status_code, 501) - - # wrong user - self._use_http_authorization('api_auth_user') - response = self.client.post('/rpc/v2/files/delete_project?project_identifier=research_project_112') - # self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_known_project_identifier(self): - response = self.client.post('/rpc/v2/files/delete_project?project_identifier=research_project_112') - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_files_are_marked_deleted(self): - files_count_before = File.objects.filter(project_identifier='research_project_112').count() - response = self.client.post('/rpc/v2/files/delete_project?project_identifier=research_project_112') - self.assertEqual(files_count_before, response.data['deleted_files_count']) - - def test_directories_are_deleted(self): - self.client.post('/rpc/v2/files/delete_project?project_identifier=research_project_112') - directories_count_after = Directory.objects.filter(project_identifier='research_project_112').count() - self.assertEqual(directories_count_after, 0) - def test_datasets_are_marked_deprecated(self): - file_ids = File.objects.filter(project_identifier='project_x').values_list('id', flat=True) - related_dataset = CatalogRecordV2.objects.filter(files__in=file_ids).distinct('id')[0] + file_ids = File.objects.filter(project_identifier="project_x").values_list("id", flat=True) + related_dataset = CatalogRecordV2.objects.filter(files__in=file_ids).distinct("id")[0] - response = self.client.post('/rpc/v2/files/delete_project?project_identifier=project_x') + response = self.client.post("/rpc/v2/files/delete_project?project_identifier=project_x") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/v2/datasets/%s' % related_dataset.identifier) - self.assertEqual(response.data['deprecated'], True) + response = self.client.get("/rest/v2/datasets/%s" % related_dataset.identifier) + self.assertEqual(response.data["deprecated"], True) diff --git a/src/metax_api/tests/api/rpc/v2/views/statistic_rpc.py b/src/metax_api/tests/api/rpc/v2/views/statistic_rpc.py index da448abf..840125a9 100755 --- a/src/metax_api/tests/api/rpc/v2/views/statistic_rpc.py +++ b/src/metax_api/tests/api/rpc/v2/views/statistic_rpc.py @@ -9,10 +9,12 @@ from django.conf import settings from django.core.management import call_command +from django.db.models import Count, Sum +from django.db.models.functions import Coalesce from rest_framework import status from rest_framework.test import APITestCase -from metax_api.models import CatalogRecord, DataCatalog +from metax_api.models import CatalogRecord, DataCatalog, File from metax_api.models.catalog_record import ACCESS_TYPES from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon from metax_api.tests.utils import TestClassUtils, test_data_file_path @@ -20,23 +22,22 @@ class StatisticRPCCommon(APITestCase, TestClassUtils): - @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ super().setUpClass() - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) def setUp(self): super().setUp() - self._use_http_authorization(username='metax') + self._use_http_authorization(username="metax") self._setup_testdata() def _setup_testdata(self): - test_orgs = ['org_1', 'org_2', 'org_3'] - access_types = [ v for k, v in ACCESS_TYPES.items() ] + test_orgs = ["org_1", "org_2", "org_3"] + access_types = [v for k, v in ACCESS_TYPES.items()] # ensure testdata has something sensible to test against. # todo may be feasible to move these to generate_test_data without breaking all tests ? @@ -53,223 +54,112 @@ def _setup_testdata(self): # distribute records between access types if cr.id % 5 == 0: - cr.research_dataset['access_rights']['access_type']['identifier'] = access_types[4] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = access_types[4] elif cr.id % 4 == 0: - cr.research_dataset['access_rights']['access_type']['identifier'] = access_types[3] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = access_types[3] elif cr.id % 3 == 0: - cr.research_dataset['access_rights']['access_type']['identifier'] = access_types[2] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = access_types[2] elif cr.id % 2 == 0: - cr.research_dataset['access_rights']['access_type']['identifier'] = access_types[1] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = access_types[1] else: - cr.research_dataset['access_rights']['access_type']['identifier'] = access_types[0] + cr.research_dataset["access_rights"]["access_type"]["identifier"] = access_types[0] # distribute records between some creation months - date = '20%s-%s-13' + date = "20%s-%s-13" if cr.id % 8 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('18', '06')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("18", "06")) elif cr.id % 7 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('18', '07')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("18", "07")) elif cr.id % 6 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('18', '10')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("18", "10")) elif cr.id % 5 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('18', '11')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("18", "11")) elif cr.id % 4 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('18', '12')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("18", "12")) elif cr.id % 3 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('19', '01')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("19", "01")) elif cr.id % 2 == 0: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('19', '02')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("19", "02")) else: - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ('19', '03')) + cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date % ("19", "03")) # set some records as "created through end user api" if cr.id % 10 == 0: cr.service_created = None - cr.user_created = 'abc%d@fairdataid' % cr.id + cr.user_created = "abc%d@fairdataid" % cr.id cr.force_save() # create a few files which do not belong to any datasets - response = self.client.get('/rest/v2/files/1', format='json') + response = self.client.get("/rest/v2/files/1", format="json") files = [] for i in range(5): f = deepcopy(response.data) - del f['id'] - f['identifier'] += 'unique' + str(i) - f['file_name'] += str(i) - f['file_path'] += str(i) - f['project_identifier'] = 'prj123' + del f["id"] + f["identifier"] += "unique" + str(i) + f["file_name"] += str(i) + f["file_path"] += str(i) + f["project_identifier"] = "prj123" files.append(f) - response = self.client.post('/rest/v2/files', files, format='json') + response = self.client.post("/rest/v2/files", files, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - response = self.client.get('/rest/v2/directories/update_byte_sizes_and_file_counts', format='json') + response = self.client.get( + "/rest/v2/directories/update_byte_sizes_and_file_counts", format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) - response = self.client.get('/rest/v2/datasets/update_cr_total_files_byte_sizes', format='json') + response = self.client.get( + "/rest/v2/datasets/update_cr_total_files_byte_sizes", format="json" + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) # create legacy datacatalog - dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema='ida').first() - dc.catalog_json['identifier'] = settings.LEGACY_CATALOGS[0] + dc = DataCatalog.objects.filter(catalog_json__research_dataset_schema="ida").first() + dc.catalog_json["identifier"] = settings.LEGACY_CATALOGS[0] dc_json = { "catalog_record_services_create": "testuser,api_auth_user,metax", "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_json": dc.catalog_json + "catalog_json": dc.catalog_json, } - response = self.client.post('/rest/v2/datacatalogs', dc_json, format="json") + response = self.client.post("/rest/v2/datacatalogs", dc_json, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - def _set_deprecated_dataset(self, id=1): - cr = CatalogRecord.objects.get(id=id) - cr.deprecated = True - cr.force_save() - - def _set_removed_dataset(self, id=1): - cr = CatalogRecord.objects.get(id=id) - cr.removed = True - cr.force_save() - - def _create_legacy_dataset(self): - """ - Creates one new legacy dataset and returns its id - """ - legacy_dataset = { - "data_catalog": { - "identifier": settings.LEGACY_CATALOGS[0] - }, - "metadata_owner_org": "some_org_id", - "metadata_provider_org": "some_org_id", - "metadata_provider_user": "some_user_id", - "research_dataset": { - "title": { - "en": "Test Dataset Title" - }, - "description": { - "en": "A descriptive description describing the contents of this dataset. Must be descriptive." - }, - "files": [ - { - "identifier": "pid:urn:1", - "title": "File Title", - "description": "informative description", - "use_category": { - "identifier": "method" - } - }, - { - "identifier": "pid:urn:3", - "title": "File Title", - "description": "informative description", - "use_category": { - "identifier": "method" - } - } - ], - "creator": [ - { - "name": "Teppo Testaaja", - "@type": "Person", - "member_of": { - "name": { - "fi": "Testiorganisaatio" - }, - "@type": "Organization" - } - } - ], - "access_rights": { - "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", - "pref_label": { - "fi": "Avoin", - "en": "Open", - "und": "Avoin" - }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type" - } - }, - "preferred_identifier": "uniikkinen_aidentifaijeri" - } - } - response = self.client.post('/rest/v2/datasets', legacy_dataset, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - - return response.data['id'] - - def _create_new_dataset(self, dataset_json): - response = self.client.post('/rest/v2/datasets', dataset_json, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - new_cr_id = response.data['id'] - - return new_cr_id - def _create_new_dataset_version(self, id=1): """ Finds the latest version of given dataset, and creates a new version of it. """ - response = self.client.get(f'/rest/v2/datasets/{id}?include_legacy', format='json') + response = self.client.get(f"/rest/v2/datasets/{id}?include_legacy", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr = response.data - while cr.get('next_dataset_version', False): - id = cr['next_dataset_version']['id'] - response = self.client.get(f'/rest/v2/datasets/{id}?include_legacy', format='json') + while cr.get("next_dataset_version", False): + id = cr["next_dataset_version"]["id"] + response = self.client.get(f"/rest/v2/datasets/{id}?include_legacy", format="json") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) cr = response.data - response = self.client.post(f'/rpc/v2/datasets/create_new_version?identifier={cr["id"]}', format='json') + response = self.client.post( + f'/rpc/v2/datasets/create_new_version?identifier={cr["id"]}', format="json" + ) self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - new_version_id = response.data['id'] + new_version_id = response.data["id"] - response = self.client.post(f'/rpc/v2/datasets/publish_dataset?identifier={new_version_id}', format='json') + response = self.client.post( + f"/rpc/v2/datasets/publish_dataset?identifier={new_version_id}", + format="json", + ) self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) return new_version_id - def _set_dataset_creation_date(self, cr_id, date): - """ - Forcibly changes the creation date for easier testing. Date-parameter is in form 'YYYY-MM-DD' - """ - - cr = CatalogRecord.objects_unfiltered.get(id=cr_id) - cr.date_created = parse_timestamp_string_to_tz_aware_datetime(date) - cr.force_save() - - def _get_catalog_record_size(self, id): - """ - Returns the size of given record id - """ - size = CatalogRecord\ - .objects_unfiltered\ - .get(id=id)\ - .research_dataset.get('total_files_byte_size', 0) - - return size - - def _get_byte_size_of_month(self, date): - """ - Returns the byte size for given date. date is in format 'YYYY-MM' - """ - query = CatalogRecord.objects_unfiltered.filter(date_created__startswith=date) - list_of_sizes = [ cr.research_dataset.get('total_files_byte_size', 0) for cr in query ] - - return sum(list_of_sizes) - - def _get_total_byte_size(self): - """ - Returns byte size of all datasets in database - """ - query = CatalogRecord.objects_unfiltered.all() - list_of_sizes = [cr.research_dataset.get('total_files_byte_size', 0) for cr in query] - - return sum(list_of_sizes) - def _get_dataset_count_after(self, date): """ Return the total count after the date provided (inclusive). date is in format 'YYYY-MM-DD' """ - return CatalogRecord.objects_unfiltered.filter(date_created__gte=f'{date}T00:00:00+03:00').count() + return CatalogRecord.objects_unfiltered.filter( + date_created__gte=f"{date}T00:00:00+03:00" + ).count() def _get_dataset_count_of_month(self, date): """ @@ -277,27 +167,6 @@ def _get_dataset_count_of_month(self, date): """ return CatalogRecord.objects_unfiltered.filter(date_created__startswith=date).count() - def _get_total_dataset_count(self): - """ - Returns dataset count of entire database - """ - return CatalogRecord.objects_unfiltered.count() - - def _set_cr_datacatalog(self, cr_id, catalog_id): - cr = CatalogRecord.objects.get(pk=cr_id) - cr.data_catalog_id = DataCatalog.objects.get(catalog_json__identifier=catalog_id).id - cr.force_save() - - def _set_dataset_as_draft(self, cr_id): - cr = CatalogRecord.objects.get(pk=cr_id) - cr.state = 'draft' - cr.force_save() - - def _set_cr_organization(self, cr_id, org): - cr = CatalogRecord.objects.get(pk=cr_id) - cr.metadata_owner_org = org - cr.force_save() - class StatisticRPCCountDatasets(StatisticRPCCommon, CatalogRecordApiWriteCommon): """ @@ -305,515 +174,27 @@ class StatisticRPCCountDatasets(StatisticRPCCommon, CatalogRecordApiWriteCommon) combinations. """ - def test_count_datasets_single(self): - """ - Tests single parameters for api. Empty removed and legacy parameters returns true AND false matches - """ - total_count = CatalogRecord.objects_unfiltered.count() - response = self.client.get('/rpc/v2/statistics/count_datasets').data - self.assertEqual(total_count, response['count'], response) - - # test removed -parameter - self._set_removed_dataset(id=2) - response = self.client.get('/rpc/v2/statistics/count_datasets?removed=true').data - self.assertEqual(response['count'], 1, response) - - response = self.client.get('/rpc/v2/statistics/count_datasets?removed=false').data - self.assertEqual(response['count'], total_count - 1, response) - - # test legacy -parameter - self._create_legacy_dataset() - total_count = CatalogRecord.objects_unfiltered.count() - response = self.client.get('/rpc/v2/statistics/count_datasets?legacy=true').data - self.assertEqual(response['count'], 1, response) - - response = self.client.get('/rpc/v2/statistics/count_datasets?legacy=false').data - self.assertEqual(response['count'], total_count - 1, response) - - # test latest -parameter - self._create_new_dataset_version() - self._create_new_dataset_version() - total_count = CatalogRecord.objects_unfiltered.count() - response = self.client.get('/rpc/v2/statistics/count_datasets?latest=false').data # returns all - self.assertEqual(response['count'], total_count, response) - - with_param = self.client.get('/rpc/v2/statistics/count_datasets?latest=true').data - without_param = self.client.get('/rpc/v2/statistics/count_datasets').data # default is true - self.assertEqual(with_param['count'], total_count - 2, with_param) - self.assertEqual(with_param['count'], without_param['count'], with_param) - - def test_count_datasets_removed_latest(self): - second_ver = self._create_new_dataset_version() - self._create_new_dataset_version(second_ver) - self._set_removed_dataset() - self._set_removed_dataset(id=second_ver) - self._set_removed_dataset(id=2) - - rem_lat = self.client.get('/rpc/v2/statistics/count_datasets?removed=true&latest=true').data - rem_not_lat = self.client.get('/rpc/v2/statistics/count_datasets?removed=true&latest=false').data - - self.assertEqual(rem_lat['count'], 1, 'Only latest versions should be checked') # id=2 - self.assertEqual(rem_not_lat['count'], 3, 'Only the prev versions should be removed') - - # create new dataset with 2 versions - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) - self._create_new_dataset_version(response.data['id']) - - not_rem_lat = self.client.get('/rpc/v2/statistics/count_datasets?removed=false&latest=true').data - not_rem_not_lat = self.client.get('/rpc/v2/statistics/count_datasets?removed=false&latest=false').data - - self.assertEqual(not_rem_lat['count'], not_rem_not_lat['count'] - 1) - - def test_count_datasets_removed_legacy(self): - self._create_legacy_dataset() - self._create_legacy_dataset() - leg_cr = self._create_legacy_dataset() - self._set_removed_dataset(leg_cr) - total_count = CatalogRecord.objects_unfiltered.count() - - rem_leg = self.client.get('/rpc/v2/statistics/count_datasets?removed=true&legacy=true').data - rem_not_leg = self.client.get('/rpc/v2/statistics/count_datasets?removed=true&legacy=false').data - not_rem_leg = self.client.get('/rpc/v2/statistics/count_datasets?removed=false&legacy=true').data - not_rem_not_leg = self.client.get('/rpc/v2/statistics/count_datasets?removed=false&legacy=false').data - - self.assertEqual(rem_leg['count'], 1) - self.assertEqual(rem_not_leg['count'], 0) - self.assertEqual(not_rem_leg['count'], 2) - self.assertEqual(not_rem_not_leg['count'], total_count - 3) - - def test_count_datasets_latest_legacy(self): - leg_cr = self._create_legacy_dataset() - self._create_new_dataset_version(leg_cr) - self._create_new_dataset_version(leg_cr) - total_count = CatalogRecord.objects_unfiltered.count() - - leg_lat = self.client.get('/rpc/v2/statistics/count_datasets?legacy=true&latest=true').data - leg_not_lat = self.client.get('/rpc/v2/statistics/count_datasets?legacy=true&latest=false').data - not_leg_not_lat = self.client.get('/rpc/v2/statistics/count_datasets?legacy=false&latest=false').data - - self.assertEqual(leg_lat['count'], 1) - self.assertEqual(leg_not_lat['count'], 3) - self.assertEqual(not_leg_not_lat['count'], total_count - 3) - def test_count_datasets_from_date(self): total_count = CatalogRecord.objects_unfiltered.count() - june_count = self._get_dataset_count_of_month('2018-06') - july_count = self._get_dataset_count_of_month('2018-07') - aug_count = self._get_dataset_count_of_month('2018-08') + june_count = self._get_dataset_count_of_month("2018-06") + july_count = self._get_dataset_count_of_month("2018-07") + aug_count = self._get_dataset_count_of_month("2018-08") - res = self.client.get('/rpc/v2/statistics/count_datasets?from_date=2018-07-01').data - self.assertEqual(res['count'], total_count - june_count) + res = self.client.get("/rpc/v2/statistics/count_datasets?from_date=2018-07-01").data + self.assertEqual(res["count"], total_count - june_count) - res = self.client.get('/rpc/v2/statistics/count_datasets?from_date=2018-09-02').data - self.assertEqual(res['count'], total_count - june_count - july_count - aug_count) + res = self.client.get("/rpc/v2/statistics/count_datasets?from_date=2018-09-02").data + self.assertEqual(res["count"], total_count - june_count - july_count - aug_count) def test_count_datasets_to_date(self): total_count = CatalogRecord.objects_unfiltered.count() - after_jan_count = self._get_dataset_count_after('2019-01-02') - after_feb_count = self._get_dataset_count_after('2019-02-02') - - res = self.client.get('/rpc/v2/statistics/count_datasets?to_date=2019-01-01').data - self.assertEqual(res['count'], total_count - after_jan_count) - - res = self.client.get('/rpc/v2/statistics/count_datasets?to_date=2019-02-01').data - self.assertEqual(res['count'], total_count - after_feb_count) - - -class StatisticRPCAllDatasetsCumulative(StatisticRPCCommon, CatalogRecordApiWriteCommon): - """ - Test suite for all_datasets_cumulative. Test only optional parameters removed, legacy and latest for now. - """ - - url = '/rpc/v2/statistics/all_datasets_cumulative' - dateparam_all = 'from_date=2018-06&to_date=2019-03' - - def test_all_datasets_cumulative(self): - """ - Basic tests for all_datasets_cumulative including parameter checks and basic functionality. - - Return values for each interval: - count: Number of datasets created in this month - ida_byte_size: size of all files in datasets created this month - count_cumulative: number of datasets from from_date to this month (including) - ida_byte_size_cumulative: size of all files in datasets created from from_date to this month (including) - """ - - # test bad query parameters - response = self.client.get(f'{self.url}') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'from_date and to_date are required') - - response = self.client.get(f'{self.url}?from_date=2019-11&to_date=bad_parameter') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'date format is YYYY-MM') - - response = self.client.get(f'{self.url}?from_date=2019-11&to_date=2019-11-15') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, 'date format is YYYY-MM') - - # test the basic functionality - june_size = self._get_byte_size_of_month('2018-06') - june_count = self._get_dataset_count_of_month('2018-06') - - july_size = self._get_byte_size_of_month('2018-07') - july_count = self._get_dataset_count_of_month('2018-07') - - march_size = self._get_byte_size_of_month('2019-03') - march_count = self._get_dataset_count_of_month('2019-03') - - total_count = self._get_total_dataset_count() - total_size = self._get_total_byte_size() - - response = self.client.get(f'{self.url}?{self.dateparam_all}').data - # ensure the counts and byte sizes are calculated correctly - self.assertEqual(response[0]['count'], june_count, response) - self.assertEqual(response[0]['ida_byte_size'], june_size, response) - self.assertEqual(response[0]['count_cumulative'], june_count, response) - self.assertEqual(response[0]['ida_byte_size_cumulative'], june_size, response) - - self.assertEqual(response[1]['count'], july_count, response) - self.assertEqual(response[1]['ida_byte_size'], july_size, response) - self.assertEqual(response[1]['count_cumulative'], june_count + july_count, response) - self.assertEqual(response[1]['ida_byte_size_cumulative'], june_size + july_size, response) - - self.assertEqual(response[-1]['count'], march_count, response) - self.assertEqual(response[-1]['ida_byte_size'], march_size, response) - self.assertEqual(response[-1]['count_cumulative'], total_count, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], total_size, response) - - # test that only datasets from beginning of from_date is counted - response = self.client.get(f'{self.url}?from_date=2018-07&to_date=2019-03').data - - self.assertEqual(response[-1]['count_cumulative'], total_count - june_count, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], total_size - june_size, response) - - def test_all_datasets_cumulative_single(self): - """ - Tests single parameters for api. Empty removed and legacy parameters returns true AND false matches - """ - total_count = self._get_total_dataset_count() - total_size = self._get_total_byte_size() - - # test removed -parameter - june_size = self._get_byte_size_of_month('2018-06') - june_count = self._get_dataset_count_of_month('2018-06') - - self._set_removed_dataset(id=8) # belongs to 2018-06, i.e. the first interval - removed_size = self._get_catalog_record_size(id=8) - - response = self.client.get(f'{self.url}?{self.dateparam_all}&removed=true').data - # ensure that only the first month (2018-06) contains dataset and that cumulative is calculated correctly - self.assertEqual(response[0]['count'], 1, response) - self.assertEqual(response[0]['ida_byte_size'], removed_size, response) - self.assertEqual(response[0]['count_cumulative'], 1, response) - self.assertEqual(response[0]['ida_byte_size_cumulative'], removed_size, response) - - self.assertEqual(response[-1]['count_cumulative'], 1, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], removed_size, response) - - response = self.client.get(f'{self.url}?{self.dateparam_all}&removed=false').data - # ensure that the correct dataset is missing from results - self.assertEqual(response[0]['count'], june_count - 1, response) - self.assertEqual(response[0]['ida_byte_size'], june_size - removed_size, response) - self.assertEqual(response[-1]['count_cumulative'], total_count - 1, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], total_size - removed_size, response) - - # test legacy -parameter - leg_cr = self._create_legacy_dataset() # legacy cr belongs to 2019-03, i.e. the last interval - self._set_dataset_creation_date(leg_cr, '2019-03-13') - - legacy_size = self._get_catalog_record_size(id=leg_cr) - - total_count = self._get_total_dataset_count() - total_size = self._get_total_byte_size() - - march_size = self._get_byte_size_of_month('2019-03') - march_count = self._get_dataset_count_of_month('2019-03') - - response = self.client.get(f'{self.url}?{self.dateparam_all}&legacy=true').data - - self.assertEqual(response[-1]['count'], 1, response) - self.assertEqual(response[-1]['ida_byte_size'], legacy_size, response) - self.assertEqual(response[-1]['count_cumulative'], 1, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], legacy_size, response) - - response = self.client.get(f'{self.url}?{self.dateparam_all}&legacy=false').data - - self.assertEqual(response[-1]['count'], march_count - 1, response) - self.assertEqual(response[-1]['ida_byte_size'], march_size - legacy_size, response) - self.assertEqual(response[-1]['count_cumulative'], total_count - 1, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], total_size - legacy_size, response) - - # test latest -parameter - # new versions will belong to 2019-03, i.e. the last interval - second = self._create_new_dataset_version() - self._set_dataset_creation_date(second, '2019-03-17') - - old_ver_size = self._get_catalog_record_size(id=1) - - total_count = self._get_total_dataset_count() - total_size = self._get_total_byte_size() - - march_size = self._get_byte_size_of_month('2019-03') - march_count = self._get_dataset_count_of_month('2019-03') - - response = self.client.get(f'{self.url}?{self.dateparam_all}&latest=false').data # returns all - self.assertEqual(response[-1]['count'], march_count, response) - self.assertEqual(response[-1]['ida_byte_size'], march_size, response) - self.assertEqual(response[-1]['count_cumulative'], total_count, response) - self.assertEqual(response[-1]['ida_byte_size_cumulative'], total_size, response) - - with_param = self.client.get(f'{self.url}?{self.dateparam_all}&latest=true').data - self.assertEqual(with_param[-1]['count'], march_count - 1, with_param) - self.assertEqual(with_param[-1]['ida_byte_size'], march_size - old_ver_size, with_param) - self.assertEqual(with_param[-1]['count_cumulative'], total_count - 1, with_param) - self.assertEqual(with_param[-1]['ida_byte_size_cumulative'], total_size - old_ver_size, response) - - # ensure that default value(true) is working as expected - without_param = self.client.get(f'{self.url}?{self.dateparam_all}').data - self.assertEqual(with_param[-1]['count'], without_param[-1]['count'], with_param) - self.assertEqual(with_param[-1]['ida_byte_size'], without_param[-1]['ida_byte_size'], with_param) - self.assertEqual(with_param[-1]['count_cumulative'], without_param[-1]['count_cumulative'], with_param) - self.assertEqual( - with_param[-1]['ida_byte_size_cumulative'], without_param[-1]['ida_byte_size_cumulative'], with_param - ) - - def test_all_datasets_cumulative_removed_latest(self): - second = self._create_new_dataset_version() - self._set_dataset_creation_date(second, '2019-03-11') - - self._set_removed_dataset(id=1) - self._set_removed_dataset(id=second) - - latest_size = self._get_catalog_record_size(id=second) - removed_size = self._get_catalog_record_size(id=1) + latest_size - removed_count = 2 - - rem_lat = self.client.get(f'{self.url}?{self.dateparam_all}&removed=true&latest=true').data - - self.assertEqual(rem_lat[-1]['count'], 1, rem_lat) # id=second - self.assertEqual(rem_lat[-1]['ida_byte_size'], latest_size, rem_lat) - self.assertEqual(rem_lat[-1]['count_cumulative'], 1, rem_lat) - self.assertEqual(rem_lat[-1]['ida_byte_size_cumulative'], latest_size, rem_lat) - - rem_not_lat = self.client.get(f'{self.url}?{self.dateparam_all}&removed=true&latest=false').data - - self.assertEqual(rem_not_lat[-1]['count'], removed_count, rem_not_lat) # id=second - self.assertEqual(rem_not_lat[-1]['ida_byte_size'], removed_size, rem_not_lat) - self.assertEqual(rem_not_lat[-1]['count_cumulative'], removed_count, rem_not_lat) - self.assertEqual(rem_not_lat[-1]['ida_byte_size_cumulative'], removed_size, rem_not_lat) - - # create new dataset with two versions, which will not be deleted - new_cr_id = self._create_new_dataset(self.cr_test_data) - self._set_dataset_creation_date(new_cr_id, '2019-01-02') - - new_cr_ver = self._create_new_dataset_version(new_cr_id) - self._set_dataset_creation_date(new_cr_ver, '2019-01-06') - - old_version_size = self._get_catalog_record_size(id=new_cr_id) - - jan_count = self._get_dataset_count_of_month('2019-01') - jan_size = self._get_byte_size_of_month('2019-01') - - total_count = self._get_total_dataset_count() - total_size = self._get_total_byte_size() - - not_rem_lat = self.client.get(f'{self.url}?{self.dateparam_all}&removed=false&latest=true').data - - # missing the removed dataset from before and dataset id='new_cr_id' - self.assertEqual(not_rem_lat[-3]['count'], jan_count - 1, not_rem_lat) - self.assertEqual(not_rem_lat[-3]['ida_byte_size'], jan_size - old_version_size, not_rem_lat) - self.assertEqual(not_rem_lat[-1]['count_cumulative'], total_count - removed_count - 1, not_rem_lat) - self.assertEqual(not_rem_lat[-1]['ida_byte_size_cumulative'], total_size - removed_size - old_version_size, - not_rem_lat) - - not_rem_not_lat = self.client.get(f'{self.url}?{self.dateparam_all}&removed=false&latest=false').data - - self.assertEqual(not_rem_not_lat[-3]['count'], jan_count, not_rem_not_lat) - self.assertEqual(not_rem_not_lat[-3]['ida_byte_size'], jan_size, not_rem_not_lat) - self.assertEqual(not_rem_not_lat[-1]['count_cumulative'], total_count - removed_count, - not_rem_not_lat) - self.assertEqual(not_rem_not_lat[-1]['ida_byte_size_cumulative'], total_size - removed_size, not_rem_not_lat) - - def test_all_datasets_cumulative_removed_legacy(self): - leg_cr_1 = self._create_legacy_dataset() - self._set_dataset_creation_date(leg_cr_1, '2018-07-03') - - leg_cr_2 = self._create_legacy_dataset() - self._set_dataset_creation_date(leg_cr_2, '2019-02-08') - self._set_removed_dataset(leg_cr_2) - - self._set_removed_dataset(id=8) # belongs to first interval, i.e. 2018-06 - - leg_non_rem_size = self._get_catalog_record_size(leg_cr_1) - leg_removed_size = self._get_catalog_record_size(leg_cr_2) - removed_size = self._get_catalog_record_size(8) - - rem_leg_count = 3 - rem_leg_size = leg_non_rem_size + leg_removed_size + removed_size - - total_count = self._get_total_dataset_count() - total_size = self._get_total_byte_size() - - june_count = self._get_dataset_count_of_month('2018-06') - june_size = self._get_byte_size_of_month('2018-06') - - feb_count = self._get_dataset_count_of_month('2019-02') - feb_size = self._get_byte_size_of_month('2019-02') - - rem_leg = self.client.get(f'{self.url}?{self.dateparam_all}&removed=true&legacy=true').data - - self.assertEqual(rem_leg[-2]['count'], 1, rem_leg) - self.assertEqual(rem_leg[-2]['ida_byte_size'], leg_removed_size, rem_leg) - self.assertEqual(rem_leg[-1]['count_cumulative'], 1, rem_leg) - self.assertEqual(rem_leg[-1]['ida_byte_size_cumulative'], leg_removed_size, rem_leg) - - rem_not_leg = self.client.get(f'{self.url}?{self.dateparam_all}&removed=true&legacy=false').data - - self.assertEqual(rem_not_leg[0]['count'], 1, rem_not_leg) - self.assertEqual(rem_not_leg[0]['ida_byte_size'], removed_size, rem_not_leg) - self.assertEqual(rem_not_leg[-1]['count_cumulative'], 1, rem_not_leg) - self.assertEqual(rem_not_leg[-1]['ida_byte_size_cumulative'], removed_size, rem_not_leg) - - not_rem_leg = self.client.get(f'{self.url}?{self.dateparam_all}&removed=false&legacy=true').data - - self.assertEqual(not_rem_leg[1]['count'], 1, not_rem_leg) - self.assertEqual(not_rem_leg[1]['ida_byte_size'], leg_non_rem_size, not_rem_leg) - self.assertEqual(not_rem_leg[-1]['count_cumulative'], 1, not_rem_leg) - self.assertEqual(not_rem_leg[-1]['ida_byte_size_cumulative'], leg_non_rem_size, not_rem_leg) - - not_rem_not_leg = self.client.get(f'{self.url}?{self.dateparam_all}&removed=false&legacy=false').data - - self.assertEqual(not_rem_not_leg[0]['count'], june_count - 1, not_rem_not_leg) - self.assertEqual(not_rem_not_leg[0]['ida_byte_size'], june_size - removed_size, not_rem_not_leg) - self.assertEqual(not_rem_not_leg[-2]['count'], feb_count - 1, not_rem_not_leg) - self.assertEqual(not_rem_not_leg[-2]['ida_byte_size'], feb_size - leg_removed_size, not_rem_not_leg) - self.assertEqual(not_rem_not_leg[-1]['count_cumulative'], total_count - rem_leg_count, not_rem_not_leg) - self.assertEqual(not_rem_not_leg[-1]['ida_byte_size_cumulative'], total_size - rem_leg_size, not_rem_not_leg) - - def test_all_datasets_cumulative_latest_legacy(self): - leg_cr = self._create_legacy_dataset() - self._set_dataset_creation_date(leg_cr, '2019-03-08') - - second = self._create_new_dataset_version(leg_cr) - self._set_dataset_creation_date(second, '2019-03-12') - - leg_cr_size = self._get_catalog_record_size(id=leg_cr) - second_size = self._get_catalog_record_size(id=second) - - legacy_count = 2 - legacy_size = leg_cr_size + second_size - - total_count = self._get_total_dataset_count() - total_size = self._get_total_byte_size() - - march_count = self._get_dataset_count_of_month('2019-03') - march_size = self._get_byte_size_of_month('2019-03') - - leg_lat = self.client.get(f'{self.url}?{self.dateparam_all}&legacy=true&latest=true').data - - self.assertEqual(leg_lat[-1]['count'], 1, leg_lat) - self.assertEqual(leg_lat[-1]['ida_byte_size'], second_size, leg_lat) - self.assertEqual(leg_lat[-1]['count_cumulative'], 1, leg_lat) - self.assertEqual(leg_lat[-1]['ida_byte_size_cumulative'], second_size, leg_lat) - - leg_not_lat = self.client.get(f'{self.url}?{self.dateparam_all}&legacy=true&latest=false').data - - self.assertEqual(leg_not_lat[-1]['count'], legacy_count, leg_not_lat) - self.assertEqual(leg_not_lat[-1]['ida_byte_size'], legacy_size, leg_not_lat) - self.assertEqual(leg_not_lat[-1]['count_cumulative'], legacy_count, leg_not_lat) - self.assertEqual(leg_not_lat[-1]['ida_byte_size_cumulative'], legacy_size, leg_not_lat) - - not_leg_not_lat = self.client.get(f'{self.url}?{self.dateparam_all}&legacy=false&latest=false').data - - self.assertEqual(not_leg_not_lat[-1]['count'], march_count - legacy_count, not_leg_not_lat) - self.assertEqual(not_leg_not_lat[-1]['ida_byte_size'], march_size - legacy_size, not_leg_not_lat) - self.assertEqual(not_leg_not_lat[-1]['count_cumulative'], total_count - legacy_count, not_leg_not_lat) - self.assertEqual(not_leg_not_lat[-1]['ida_byte_size_cumulative'], total_size - legacy_size, not_leg_not_lat) - - -class StatisticRPCforDrafts(StatisticRPCCommon, CatalogRecordApiWriteCommon): - """ - Tests that drafts are not taken into account when calculating statistics - """ - def test_count_datasets_api_for_drafts(self): - """ - Tests that rpc/statistics/count_datasets returns only count of published datasets - """ - response_1 = self.client.get('/rpc/v2/statistics/count_datasets').data - - self._set_dataset_as_draft(1) - self.assertEqual(CatalogRecord.objects.get(pk=1).state, 'draft', - 'Dataset with id=1 should have changed state to draft') - - response_2 = self.client.get('/rpc/v2/statistics/count_datasets').data - self.assertNotEqual(response_1['count'], response_2['count'], - 'Drafts should not be returned in count_datasets api') - - def test_all_datasets_cumulative_for_drafts(self): - """ - Tests that /rpc/v2/statistics/all_datasets_cumulative returns only published datasets - """ - url = '/rpc/v2/statistics/all_datasets_cumulative?from_date=2019-06&to_date=2019-06' - - self._set_dataset_creation_date(1, '2019-06-15') - response_1 = self.client.get(url).data - - self._set_dataset_as_draft(1) - response_2 = self.client.get(url).data - - # ensure the counts and byte sizes are calculated without drafts - self.assertNotEqual(response_1[0]['count'], response_2[0]['count'], - 'Count for June should reduce by one as dataset id=1 was set as draft') - self.assertNotEqual(response_1[0]['ida_byte_size'], response_2[0]['ida_byte_size'], - 'Byte size for June should reduce by one as dataset id=1 was set as draft') - - def test_catalog_datasets_cumulative_for_drafts(self): - """ - Tests that /rpc/v2/statistics/catalog_datasets_cumulative returns only published datasets - """ - - url = '/rpc/v2/statistics/catalog_datasets_cumulative?from_date=2019-06-01&to_date=2019-06-30' - catalog = "urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d3" - - self._set_dataset_creation_date(1, '2019-06-15') - self._set_cr_datacatalog(1, catalog) # Adds id=1 to catalog - - count_1 = self.client.get(url).data[catalog]['open'][0]['count'] - total_1 = self.client.get(url).data[catalog]['total'] - - self._set_dataset_as_draft(1) - - count_2 = self.client.get(url).data[catalog]['open'][0]['count'] - total_2 = self.client.get(url).data[catalog]['total'] - - # ensure the count and total are calculated without drafts - self.assertNotEqual(count_1, count_2, 'Count should reduce by one as dataset id=1 was set as draft') - self.assertNotEqual(total_1, total_2, 'Total should reduce by one as dataset id=1 was set as draft') - - def test_end_user_datasets_cumulative_for_drafts(self): - ''' End user api should return only published data ''' - url = '/rpc/v2/statistics/end_user_datasets_cumulative?from_date=2019-06-01&to_date=2019-06-30' - - self._set_dataset_creation_date(10, '2019-06-15') - count_1 = self.client.get(url).data[0]['count'] - - self._set_dataset_as_draft(10) - count_2 = self.client.get(url).data[0]['count'] - - # ensure the count are calculated without drafts - self.assertNotEqual(count_1, count_2, 'Count should be reduced by one after setting id=10 as draft') - - def test_organization_datasets_cumulative_for_drafts(self): - ''' Organization api should return only published data ''' - url = "/rpc/v2/statistics/organization_datasets_cumulative?from_date=2019-06-01&to_date=2019-06-30" - - self._set_dataset_creation_date(1, '2019-06-15') - self._set_cr_organization(1, 'org_2') - total_1 = self.client.get(url).data['org_2']['total'] + after_jan_count = self._get_dataset_count_after("2019-01-02") + after_feb_count = self._get_dataset_count_after("2019-02-02") - self._set_dataset_as_draft(1) - total_2 = self.client.get(url).data['org_2']['total'] + res = self.client.get("/rpc/v2/statistics/count_datasets?to_date=2019-01-01").data + self.assertEqual(res["count"], total_count - after_jan_count) - # ensure the totals are calculated without drafts - self.assertNotEqual(total_1, total_2, 'Count be reduced by one after setting id=1 as draft') \ No newline at end of file + res = self.client.get("/rpc/v2/statistics/count_datasets?to_date=2019-02-01").data + self.assertEqual(res["count"], total_count - after_feb_count) \ No newline at end of file diff --git a/src/metax_api/tests/management/commands/loadinitialdata.py b/src/metax_api/tests/management/commands/loadinitialdata.py index 2d64f295..fcc576b2 100755 --- a/src/metax_api/tests/management/commands/loadinitialdata.py +++ b/src/metax_api/tests/management/commands/loadinitialdata.py @@ -24,8 +24,8 @@ class LoadInitialDataTest(LiveServerTestCase): def setUp(self, *args, **kwargs): self._test_settings = { - 'metax_url': self.live_server_url, - 'metax_credentials': settings.API_METAX_USER, + "metax_url": self.live_server_url, + "metax_credentials": settings.API_METAX_USER, } super(LoadInitialDataTest, self).setUp(*args, **kwargs) @@ -34,11 +34,17 @@ def test_create(self): storages_before = FileStorage.objects.all().count() out = StringIO() - call_command('loadinitialdata', **{ 'test_settings': self._test_settings, 'stdout': out, }) + call_command( + "loadinitialdata", + **{ + "test_settings": self._test_settings, + "stdout": out, + }, + ) cmd_output = out.getvalue() - self.assertIn('Created catalog', cmd_output) - self.assertIn('Created file storage', cmd_output) + self.assertIn("Created catalog", cmd_output) + self.assertIn("Created file storage", cmd_output) self.assertEqual(catalogs_before < DataCatalog.objects.all().count(), True) self.assertEqual(storages_before < FileStorage.objects.all().count(), True) @@ -48,10 +54,22 @@ def test_update(self): """ out = StringIO() # create - call_command('loadinitialdata', **{ 'test_settings': self._test_settings, 'stdout': out, }) + call_command( + "loadinitialdata", + **{ + "test_settings": self._test_settings, + "stdout": out, + }, + ) # update - call_command('loadinitialdata', **{ 'test_settings': self._test_settings, 'stdout': out, }) + call_command( + "loadinitialdata", + **{ + "test_settings": self._test_settings, + "stdout": out, + }, + ) cmd_output = out.getvalue() - self.assertIn('Updated catalog', cmd_output) - self.assertIn('Updated file storage', cmd_output) + self.assertIn("Updated catalog", cmd_output) + self.assertIn("Updated file storage", cmd_output) diff --git a/src/metax_api/tests/middleware/test_middleware.py b/src/metax_api/tests/middleware/test_middleware.py index ce5dd2a0..45fdb24b 100755 --- a/src/metax_api/tests/middleware/test_middleware.py +++ b/src/metax_api/tests/middleware/test_middleware.py @@ -44,18 +44,18 @@ def test_authorization_not_required(self): # reset credentials self.client.credentials() - response = self.client.get('/rest/datasets') + response = self.client.get("/rest/datasets") self.assertNotEqual(response.status_code, FORBIDDEN) - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertNotEqual(response.status_code, FORBIDDEN) def test_optional_authorizatiaon_during_get(self): """ If auth headers are passed during GET, the user should then be identified by them. """ - response = self.client.get('/rest/datasets') + response = self.client.get("/rest/datasets") self.assertNotEqual(response.status_code, FORBIDDEN) - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self.assertNotEqual(response.status_code, FORBIDDEN) def test_optional_authorizatiaon_during_get_fails(self): @@ -63,8 +63,8 @@ def test_optional_authorizatiaon_during_get_fails(self): If auth headers are passed during GET, the user should then be identified by them. And if credentials are wrong, then access is forbidden """ - self._use_http_authorization(username='nope', password='wrong') - response = self.client.get('/rest/datasets') + self._use_http_authorization(username="nope", password="wrong") + response = self.client.get("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) # @@ -81,13 +81,13 @@ def test_authorization_ok(self): The following requests are invalid by their content, but none should fail to the very first step of identifying the api caller. """ - response = self.client.post('/rest/datasets') + response = self.client.post("/rest/datasets") self.assertNotEqual(response.status_code, FORBIDDEN) - response = self.client.put('/rest/datasets') + response = self.client.put("/rest/datasets") self.assertNotEqual(response.status_code, FORBIDDEN) - response = self.client.patch('/rest/datasets') + response = self.client.patch("/rest/datasets") self.assertNotEqual(response.status_code, FORBIDDEN) - response = self.client.delete('/rest/datasets') + response = self.client.delete("/rest/datasets") self.assertNotEqual(response.status_code, FORBIDDEN) def test_unknown_user(self): @@ -95,25 +95,25 @@ def test_unknown_user(self): Unknown user credentials, every request should fail to the very first step of identifying the api caller. """ - self._use_http_authorization(username='other', password='pw') - response = self.client.post('/rest/datasets') + self._use_http_authorization(username="other", password="pw") + response = self.client.post("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.put('/rest/datasets') + response = self.client.put("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.patch('/rest/datasets') + response = self.client.patch("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.delete('/rest/datasets') + response = self.client.delete("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) def test_wrong_password(self): - self._use_http_authorization(password='wrongpassword') - response = self.client.post('/rest/datasets') + self._use_http_authorization(password="wrongpassword") + response = self.client.post("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.put('/rest/datasets') + response = self.client.put("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.patch('/rest/datasets') + response = self.client.patch("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.delete('/rest/datasets') + response = self.client.delete("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) # @@ -128,74 +128,80 @@ def test_wrong_password(self): # def test_malformed_auth_header(self): - self._use_http_authorization(header_value='Basic hubbabubba') - response = self.client.post('/rest/datasets') + self._use_http_authorization(header_value="Basic hubbabubba") + response = self.client.post("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) def test_invalid_auth_method(self): - self._use_http_authorization(header_value='NotSupported hubbabubba') - response = self.client.post('/rest/datasets') + self._use_http_authorization(header_value="NotSupported hubbabubba") + response = self.client.post("/rest/datasets") self.assertEqual(response.status_code, FORBIDDEN) class ApiModifyResponseTestV1(CatalogRecordApiWriteCommon): - def test_catalog_record_get_last_modified_header(self): - response = self.client.get('/rest/datasets/1') + response = self.client.get("/rest/datasets/1") self._validate_response(response) def test_catalog_record_post_last_modified_header(self): - response = self.client.post('/rest/datasets', self.cr_test_data, format="json") + response = self.client.post("/rest/datasets", self.cr_test_data, format="json") self._validate_response(response) def test_catalog_record_put_last_modified_header(self): - cr = self.client.get('/rest/datasets/1', format="json").data - cr['preservation_description'] = 'what' - response = self.client.put('/rest/datasets/1', cr, format="json") + cr = self.client.get("/rest/datasets/1", format="json").data + cr["preservation_description"] = "what" + response = self.client.put("/rest/datasets/1", cr, format="json") self._validate_response(response) def test_catalog_record_patch_last_modified_header(self): - cr = self.client.get('/rest/datasets/1', format="json").data - cr['preservation_description'] = 'what' - response = self.client.patch('/rest/datasets/1', cr, format="json") + cr = self.client.get("/rest/datasets/1", format="json").data + cr["preservation_description"] = "what" + response = self.client.patch("/rest/datasets/1", cr, format="json") self._validate_response(response) def test_catalog_record_delete_does_not_contain_last_modified_header(self): - response = self.client.delete('/rest/datasets/1') - self.assertFalse(response.has_header('Last-Modified')) + response = self.client.delete("/rest/datasets/1") + self.assertFalse(response.has_header("Last-Modified")) def test_catalog_record_bulk_create_get_last_modified_header(self): - response = self.client.post('/rest/datasets', [self.cr_test_data, self.cr_test_data], format="json") + response = self.client.post( + "/rest/datasets", [self.cr_test_data, self.cr_test_data], format="json" + ) self._validate_response(response) def _validate_response(self, response): - data = response.data.get('success', response.data) - obj = data[0].get('object', None) if isinstance(data, list) else data + data = response.data.get("success", response.data) + obj = data[0].get("object", None) if isinstance(data, list) else data self.assertIsNotNone(obj) - expected_modified_str = obj['date_modified'] if 'date_modified' in obj else obj.get('date_created', None) - expected_modified = timezone.localtime(parse_timestamp_string_to_tz_aware_datetime(expected_modified_str), - timezone=tz('GMT')) + expected_modified_str = ( + obj["date_modified"] if "date_modified" in obj else obj.get("date_created", None) + ) + expected_modified = timezone.localtime( + parse_timestamp_string_to_tz_aware_datetime(expected_modified_str), + timezone=tz("GMT"), + ) - self.assertTrue(response.has_header('Last-Modified')) - actual_modified = timezone.localtime(parse_timestamp_string_to_tz_aware_datetime(response.get('Last-Modified')), - timezone=tz('GMT')) + self.assertTrue(response.has_header("Last-Modified")) + actual_modified = timezone.localtime( + parse_timestamp_string_to_tz_aware_datetime(response.get("Last-Modified")), + timezone=tz("GMT"), + ) self.assertEqual(expected_modified, actual_modified) class ApiStreamHttpResponse(CatalogRecordApiWriteCommon): - def test_no_streaming_with_paging(self): - response = self.client.get('/rest/datasets?stream=true') + response = self.client.get("/rest/datasets?stream=true") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.streaming, False) def test_streaming_json(self): - response = self.client.get('/rest/datasets?pagination=false&stream=true') + response = self.client.get("/rest/datasets?pagination=false&stream=true") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.streaming, True) - response = self.client.get('/rest/files?pagination=false&stream=true') + response = self.client.get("/rest/files?pagination=false&stream=true") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.streaming, True) diff --git a/src/metax_api/tests/middleware/test_middleware_v2.py b/src/metax_api/tests/middleware/test_middleware_v2.py deleted file mode 100755 index fb465f84..00000000 --- a/src/metax_api/tests/middleware/test_middleware_v2.py +++ /dev/null @@ -1,200 +0,0 @@ -# This file is part of the Metax API service -# -# Copyright 2017-2018 Ministry of Education and Culture, Finland -# -# :author: CSC - IT Center for Science Ltd., Espoo Finland -# :license: MIT - -from django.utils import timezone -from pytz import timezone as tz -from rest_framework import status -from rest_framework.test import APITestCase - -from metax_api.tests.api.rest.base.views.datasets.write import CatalogRecordApiWriteCommon -from metax_api.tests.utils import TestClassUtils -from metax_api.utils import parse_timestamp_string_to_tz_aware_datetime - -FORBIDDEN = status.HTTP_403_FORBIDDEN - - -class ApiAuthnzTest(APITestCase, TestClassUtils): - """ - Test use of HTTP Authorization header for authnz for POST, PUT, PATCH and - DELETE requests. API caller identification is performed as the first step - of each request, so any API is as good as any, even with invalid payload, - for testing purposes. - """ - - def setUp(self): - self._use_http_authorization() - - # - # - # - # read requests - # - # - # - - def test_authorization_not_required(self): - """ - GET operations are allowed for all. - """ - - # reset credentials - self.client.credentials() - - response = self.client.get('/rest/v2/datasets') - self.assertNotEqual(response.status_code, FORBIDDEN) - response = self.client.get('/rest/v2/datasets/1') - self.assertNotEqual(response.status_code, FORBIDDEN) - - def test_optional_authorizatiaon_during_get(self): - """ - If auth headers are passed during GET, the user should then be identified by them. - """ - response = self.client.get('/rest/v2/datasets') - self.assertNotEqual(response.status_code, FORBIDDEN) - response = self.client.get('/rest/v2/datasets/1') - self.assertNotEqual(response.status_code, FORBIDDEN) - - def test_optional_authorizatiaon_during_get_fails(self): - """ - If auth headers are passed during GET, the user should then be identified by them. - And if credentials are wrong, then access is forbidden - """ - self._use_http_authorization(username='nope', password='wrong') - response = self.client.get('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - - # - # - # - # write requests - # - # - # - - def test_authorization_ok(self): - """ - All write operations require proper authnz using HTTP Authorization header. - The following requests are invalid by their content, but none should fail to - the very first step of identifying the api caller. - """ - response = self.client.post('/rest/v2/datasets') - self.assertNotEqual(response.status_code, FORBIDDEN) - response = self.client.put('/rest/v2/datasets') - self.assertNotEqual(response.status_code, FORBIDDEN) - response = self.client.patch('/rest/v2/datasets') - self.assertNotEqual(response.status_code, FORBIDDEN) - response = self.client.delete('/rest/v2/datasets') - self.assertNotEqual(response.status_code, FORBIDDEN) - - def test_unknown_user(self): - """ - Unknown user credentials, every request should fail to the very first step - of identifying the api caller. - """ - self._use_http_authorization(username='other', password='pw') - response = self.client.post('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.put('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.patch('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.delete('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - - def test_wrong_password(self): - self._use_http_authorization(password='wrongpassword') - response = self.client.post('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.put('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.patch('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - response = self.client.delete('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - - # - # - # - # invalid auth header - # - # All errors during the auth header processing should return 403, without - # giving more specific errors. - # - # - # - - def test_malformed_auth_header(self): - self._use_http_authorization(header_value='Basic hubbabubba') - response = self.client.post('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - - def test_invalid_auth_method(self): - self._use_http_authorization(header_value='NotSupported hubbabubba') - response = self.client.post('/rest/v2/datasets') - self.assertEqual(response.status_code, FORBIDDEN) - - -class ApiModifyResponseTest(CatalogRecordApiWriteCommon): - - def test_catalog_record_get_last_modified_header(self): - response = self.client.get('/rest/v2/datasets/1') - self._validate_response(response) - - def test_catalog_record_post_last_modified_header(self): - response = self.client.post('/rest/v2/datasets', self.cr_test_data, format="json") - self._validate_response(response) - - def test_catalog_record_put_last_modified_header(self): - cr = self.client.get('/rest/v2/datasets/1', format="json").data - cr['preservation_description'] = 'what' - response = self.client.put('/rest/v2/datasets/1', cr, format="json") - self._validate_response(response) - - def test_catalog_record_patch_last_modified_header(self): - cr = self.client.get('/rest/v2/datasets/1', format="json").data - cr['preservation_description'] = 'what' - response = self.client.patch('/rest/v2/datasets/1', cr, format="json") - self._validate_response(response) - - def test_catalog_record_delete_does_not_contain_last_modified_header(self): - response = self.client.delete('/rest/v2/datasets/1') - self.assertFalse(response.has_header('Last-Modified')) - - def test_catalog_record_bulk_create_get_last_modified_header(self): - response = self.client.post('/rest/v2/datasets', [self.cr_test_data, self.cr_test_data], format="json") - self._validate_response(response) - - def _validate_response(self, response): - data = response.data.get('success', response.data) - obj = data[0].get('object', None) if isinstance(data, list) else data - self.assertIsNotNone(obj) - - expected_modified_str = obj['date_modified'] if 'date_modified' in obj else obj.get('date_created', None) - expected_modified = timezone.localtime(parse_timestamp_string_to_tz_aware_datetime(expected_modified_str), - timezone=tz('GMT')) - - self.assertTrue(response.has_header('Last-Modified')) - actual_modified = timezone.localtime(parse_timestamp_string_to_tz_aware_datetime(response.get('Last-Modified')), - timezone=tz('GMT')) - - self.assertEqual(expected_modified, actual_modified) - - -class ApiStreamHttpResponse(CatalogRecordApiWriteCommon): - - def test_no_streaming_with_paging(self): - response = self.client.get('/rest/v2/datasets?stream=true') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.streaming, False) - - def test_streaming_json(self): - response = self.client.get('/rest/v2/datasets?no_pagination=true&stream=true') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.streaming, True) - response = self.client.get('/rest/v2/files?no_pagination=true&stream=true') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.streaming, True) diff --git a/src/metax_api/tests/models/__init__.py b/src/metax_api/tests/models/__init__.py index 2c2ac851..d8b29da6 100755 --- a/src/metax_api/tests/models/__init__.py +++ b/src/metax_api/tests/models/__init__.py @@ -5,8 +5,13 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT -from .catalog_record import CatalogRecordModelBasicTest, CatalogRecordModelTests, CatalogRecordManagerTests +from .catalog_record import ( + CatalogRecordModelBasicTest, + CatalogRecordModelTests, + CatalogRecordManagerTests, +) from .common import * from .data_catalog import DataCatalogModelTests from .directory import DirectoryModelTests from .file import FileModelBasicTest, FileManagerTests +from .signals import SignalTests diff --git a/src/metax_api/tests/models/catalog_record.py b/src/metax_api/tests/models/catalog_record.py index da77689c..5a3bbe61 100755 --- a/src/metax_api/tests/models/catalog_record.py +++ b/src/metax_api/tests/models/catalog_record.py @@ -23,22 +23,28 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(CatalogRecordModelBasicTest, cls).setUpClass() def setUp(self): - dataset_from_test_data = self._get_object_from_test_data('catalogrecord') - self.metadata_version_identifier = dataset_from_test_data['research_dataset']['metadata_version_identifier'] - self.identifier = dataset_from_test_data['identifier'] + dataset_from_test_data = self._get_object_from_test_data("catalogrecord") + self.metadata_version_identifier = dataset_from_test_data["research_dataset"][ + "metadata_version_identifier" + ] + self.identifier = dataset_from_test_data["identifier"] def test_get_by_identifiers(self): catalog_record = CatalogRecord.objects.get(identifier=self.identifier) self.assertEqual(catalog_record.identifier, self.identifier) catalog_record = CatalogRecord.objects.get( - research_dataset__contains={'metadata_version_identifier': self.metadata_version_identifier} + research_dataset__contains={ + "metadata_version_identifier": self.metadata_version_identifier + } + ) + self.assertEqual( + catalog_record.metadata_version_identifier, self.metadata_version_identifier ) - self.assertEqual(catalog_record.metadata_version_identifier, self.metadata_version_identifier) class CatalogRecordModelTests(TestCase, TestClassUtils): @@ -47,7 +53,7 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(CatalogRecordModelTests, cls).setUpClass() def setUp(self): @@ -55,53 +61,58 @@ def setUp(self): def test_disallow_total_files_byte_size_manual_update(self): cr = self.cr - old = cr.research_dataset['total_files_byte_size'] - cr.research_dataset['total_files_byte_size'] = 999 + old = cr.research_dataset["total_files_byte_size"] + cr.research_dataset["total_files_byte_size"] = 999 cr.save() - self.assertEqual(old, cr.research_dataset['total_files_byte_size']) + self.assertEqual(old, cr.research_dataset["total_files_byte_size"]) def test_disallow_metadata_version_identifier_manual_update(self): cr = self.cr - old = cr.research_dataset['metadata_version_identifier'] - cr.research_dataset['metadata_version_identifier'] = 'changed' + old = cr.research_dataset["metadata_version_identifier"] + cr.research_dataset["metadata_version_identifier"] = "changed" cr.save() - self.assertEqual(old, cr.research_dataset['metadata_version_identifier']) + self.assertEqual(old, cr.research_dataset["metadata_version_identifier"]) def test_total_files_byte_size_auto_update_on_files_changed(self): """ Changing files of a dataset creates a new version, so make sure that the file size of the old version does NOT change, and the file size of the new version DOES change. """ - already_included_files = CatalogRecord.objects.get(pk=1).files.all().values_list('id', flat=True) + already_included_files = ( + CatalogRecord.objects.get(pk=1).files.all().values_list("id", flat=True) + ) new_file_id = File.objects.all().exclude(id__in=already_included_files).first().id - file_from_testdata = self._get_object_from_test_data('file', requested_index=new_file_id) + file_from_testdata = self._get_object_from_test_data("file", requested_index=new_file_id) cr = CatalogRecord.objects.get(pk=1) - old = cr.research_dataset['total_files_byte_size'] - cr.research_dataset['files'] = [file_from_testdata] + old = cr.research_dataset["total_files_byte_size"] + cr.research_dataset["files"] = [file_from_testdata] cr.save() new_version = cr.next_dataset_version - self.assertEqual(old, cr.research_dataset['total_files_byte_size']) - self.assertNotEqual(old, new_version.research_dataset['total_files_byte_size']) + self.assertEqual(old, cr.research_dataset["total_files_byte_size"]) + self.assertNotEqual(old, new_version.research_dataset["total_files_byte_size"]) def test_preservation_state_modified_auto_update(self): cr = self.cr old = cr.preservation_state_modified cr.preservation_state = 1 cr.save() - self.assertNotEqual(old, cr.preservation_state_modified, - 'preservation_state_modified should be automatically updated if changed') + self.assertNotEqual( + old, + cr.preservation_state_modified, + "preservation_state_modified should be automatically updated if changed", + ) class CatalogRecordManagerTests(TestCase, TestClassUtils): @classmethod def setUpClass(cls): - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(CatalogRecordManagerTests, cls).setUpClass() def test_get_using_dict_with_id(self): - row = {'id': 1, 'other_stuff': 'doesnt matter'} + row = {"id": 1, "other_stuff": "doesnt matter"} try: obj = CatalogRecord.objects.get(using_dict=row) except CatalogRecord.DoesNotExist: @@ -109,15 +120,15 @@ def test_get_using_dict_with_id(self): else: found = True - self.assertEqual(found, True, 'get with using_dict should have returned a result') + self.assertEqual(found, True, "get with using_dict should have returned a result") self.assertEqual(obj.id, 1) def test_get_using_dict_with_metadata_version_identifier(self): row = { - 'research_dataset': { - 'metadata_version_identifier': CatalogRecord.objects.first().metadata_version_identifier + "research_dataset": { + "metadata_version_identifier": CatalogRecord.objects.first().metadata_version_identifier }, - 'other_stuff': 'doesnt matter' + "other_stuff": "doesnt matter", } try: obj = CatalogRecord.objects.get(using_dict=row) @@ -126,11 +137,11 @@ def test_get_using_dict_with_metadata_version_identifier(self): else: found = True - self.assertEqual(found, True, 'get with using_dict should have returned a result') + self.assertEqual(found, True, "get with using_dict should have returned a result") self.assertEqual(obj.id, 1) def test_get_using_dict_error_not_found_1(self): - row = {'id': 101010, 'other_stuff': 'doesnt matter'} + row = {"id": 101010, "other_stuff": "doesnt matter"} try: CatalogRecord.objects.get(using_dict=row) except CatalogRecord.DoesNotExist: @@ -138,11 +149,15 @@ def test_get_using_dict_error_not_found_1(self): else: found = True - self.assertEqual(found, False, 'get with using_dict should have not returned a result') + self.assertEqual(found, False, "get with using_dict should have not returned a result") def test_get_using_dict_error_preferred_identifier_not_allowed(self): - row = {'research_dataset': {'preferred_identifier': CatalogRecord.objects.first().preferred_identifier}, - 'other_stuff': 'doesnt matter'} + row = { + "research_dataset": { + "preferred_identifier": CatalogRecord.objects.first().preferred_identifier + }, + "other_stuff": "doesnt matter", + } try: CatalogRecord.objects.get(using_dict=row) except ValidationError: @@ -150,11 +165,14 @@ def test_get_using_dict_error_preferred_identifier_not_allowed(self): else: found = True - self.assertEqual(found, False, - 'get with using_dict should have not returned a result, because preferred_identifier was used') + self.assertEqual( + found, + False, + "get with using_dict should have not returned a result, because preferred_identifier was used", + ) def test_get_using_dict_error_identifier_field_missing(self): - row = {'somefield': 111, 'other_stuff': 'doesnt matter'} + row = {"somefield": 111, "other_stuff": "doesnt matter"} try: CatalogRecord.objects.get(using_dict=row) except ValidationError: @@ -162,5 +180,8 @@ def test_get_using_dict_error_identifier_field_missing(self): else: found = True - self.assertEqual(found, False, - 'get with using_dict should have not returned a result because an identifier field is missing') + self.assertEqual( + found, + False, + "get with using_dict should have not returned a result because an identifier field is missing", + ) diff --git a/src/metax_api/tests/models/common.py b/src/metax_api/tests/models/common.py index f9a73ffe..3d008cd4 100755 --- a/src/metax_api/tests/models/common.py +++ b/src/metax_api/tests/models/common.py @@ -14,9 +14,8 @@ class CommonModelTests(TestCase, TestClassUtils): - def setUp(self): - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) def get(self): return CatalogRecord.objects.get(pk=1) @@ -37,7 +36,7 @@ def test_field_changed_ok(self): cr = self.get() cr.preservation_state = 2 - self.assertEqual(cr.field_changed('preservation_state'), True) + self.assertEqual(cr.field_changed("preservation_state"), True) def test_field_not_changed_ok(self): cr = self.get() @@ -46,12 +45,15 @@ def test_field_not_changed_ok(self): cr = self.get() cr.preservation_state = 1 - self.assertEqual(cr.field_changed('preservation_state'), False) + self.assertEqual(cr.field_changed("preservation_state"), False) def test_field_is_not_tracked(self): cr = self.get() - with self.assertRaises(FieldError, msg='field is not tracked, so checking changes should be an error'): - cr.field_changed('user_modified') + with self.assertRaises( + FieldError, + msg="field is not tracked, so checking changes should be an error", + ): + cr.field_changed("user_modified") def test_field_is_tracked_but_not_loaded(self): """ @@ -62,20 +64,23 @@ def test_field_is_tracked_but_not_loaded(self): However, if the field does end up being modified, then the caller should make sure the field is included when the object is being created. """ - queryset = CatalogRecord.objects.filter(pk=1).only('id') + queryset = CatalogRecord.objects.filter(pk=1).only("id") cr = queryset[0] cr.preservation_state = 2 - with self.assertRaises(FieldError, msg='field is not loaded in init, so checking changes should be an error'): - cr.field_changed('preservation_state') + with self.assertRaises( + FieldError, + msg="field is not loaded in init, so checking changes should be an error", + ): + cr.field_changed("preservation_state") def test_field_is_tracked_and_explicitly_loaded(self): """ Same as above, but the caller makes sure to include the field in the ORM query. """ - queryset = CatalogRecord.objects.filter(pk=1).only('preservation_state') + queryset = CatalogRecord.objects.filter(pk=1).only("preservation_state") cr = queryset[0] cr.preservation_state = 2 - self.assertEqual(cr.field_changed('preservation_state'), True) + self.assertEqual(cr.field_changed("preservation_state"), True) class CommonModelTrackJsonFieldsTests(CommonModelTests): @@ -87,17 +92,20 @@ class CommonModelTrackJsonFieldsTests(CommonModelTests): def test_json_field_changed_ok(self): cr = self.get() - cr.research_dataset['preferred_identifier'] = 'new' - self.assertEqual(cr.field_changed('research_dataset.preferred_identifier'), True) + cr.research_dataset["preferred_identifier"] = "new" + self.assertEqual(cr.field_changed("research_dataset.preferred_identifier"), True) def test_json_field_not_changed_ok(self): cr = self.get() - cr.research_dataset['preferred_identifier'] = cr.research_dataset['preferred_identifier'] - self.assertEqual(cr.field_changed('research_dataset.preferred_identifier'), False) + cr.research_dataset["preferred_identifier"] = cr.research_dataset["preferred_identifier"] + self.assertEqual(cr.field_changed("research_dataset.preferred_identifier"), False) def test_json_field_is_tracked_but_not_loaded(self): - queryset = CatalogRecord.objects.filter(pk=1).only('id') + queryset = CatalogRecord.objects.filter(pk=1).only("id") cr = queryset[0] - cr.research_dataset['preferred_identifier'] = cr.research_dataset['preferred_identifier'] - with self.assertRaises(FieldError, msg='field is not loaded in init, so checking changes should be an error'): - cr.field_changed('research_dataset.preferred_identifier') + cr.research_dataset["preferred_identifier"] = cr.research_dataset["preferred_identifier"] + with self.assertRaises( + FieldError, + msg="field is not loaded in init, so checking changes should be an error", + ): + cr.field_changed("research_dataset.preferred_identifier") diff --git a/src/metax_api/tests/models/data_catalog.py b/src/metax_api/tests/models/data_catalog.py index a0844470..8f3fc5a9 100755 --- a/src/metax_api/tests/models/data_catalog.py +++ b/src/metax_api/tests/models/data_catalog.py @@ -18,7 +18,7 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(DataCatalogModelTests, cls).setUpClass() def setUp(self): @@ -26,7 +26,7 @@ def setUp(self): def test_disallow_identifier_manual_update(self): dc = self.dc - old = dc.catalog_json['identifier'] - dc.catalog_json['identifier'] = 'changed value' + old = dc.catalog_json["identifier"] + dc.catalog_json["identifier"] = "changed value" dc.save() - self.assertEqual(old, dc.catalog_json['identifier']) + self.assertEqual(old, dc.catalog_json["identifier"]) diff --git a/src/metax_api/tests/models/directory.py b/src/metax_api/tests/models/directory.py index d1948a9c..18917438 100755 --- a/src/metax_api/tests/models/directory.py +++ b/src/metax_api/tests/models/directory.py @@ -14,13 +14,12 @@ class DirectoryModelTests(APITestCase, TestClassUtils): - @classmethod def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(DirectoryModelTests, cls).setUpClass() def setUp(self): @@ -34,13 +33,16 @@ def test_calculate_byte_size_and_file_count(self): for root_dir in Directory.objects.filter(parent_directory_id=None): root_dir.calculate_byte_size_and_file_count() - byte_size = File.objects.filter(project_identifier=root_dir.project_identifier) \ - .aggregate(Sum('byte_size'))['byte_size__sum'] + byte_size = File.objects.filter( + project_identifier=root_dir.project_identifier + ).aggregate(Sum("byte_size"))["byte_size__sum"] file_count = File.objects.filter(project_identifier=root_dir.project_identifier).count() - response = self.client.get('/rest/directories/root?project=%s' % root_dir.project_identifier) - self.assertEqual(response.data['byte_size'], byte_size) - self.assertEqual(response.data['file_count'], file_count) + response = self.client.get( + "/rest/directories/root?project=%s" % root_dir.project_identifier + ) + self.assertEqual(response.data["byte_size"], byte_size) + self.assertEqual(response.data["file_count"], file_count) def test_disallow_calculate_byte_size_and_file_count_for_non_root(self): with self.assertRaises(Exception): diff --git a/src/metax_api/tests/models/file.py b/src/metax_api/tests/models/file.py index 5f04040e..ce6b4f0f 100755 --- a/src/metax_api/tests/models/file.py +++ b/src/metax_api/tests/models/file.py @@ -25,13 +25,13 @@ def setUpClass(cls): """ Loaded only once for test cases inside this class. """ - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(FileModelBasicTest, cls).setUpClass() def setUp(self): - file_from_test_data = self._get_object_from_test_data('file') - self.identifier = file_from_test_data['identifier'] - self.result_file_name = file_from_test_data['file_name'] + file_from_test_data = self._get_object_from_test_data("file") + self.identifier = file_from_test_data["identifier"] + self.result_file_name = file_from_test_data["file_name"] def test_get_by_identifier(self): file = File.objects.get(identifier=self.identifier) @@ -41,16 +41,16 @@ def test_get_by_identifier(self): class FileManagerTests(TestCase, TestClassUtils): @classmethod def setUpClass(cls): - call_command('loaddata', test_data_file_path, verbosity=0) + call_command("loaddata", test_data_file_path, verbosity=0) super(FileManagerTests, cls).setUpClass() def setUp(self): - file_from_test_data = self._get_object_from_test_data('file', requested_index=0) - self.identifier = file_from_test_data['identifier'] - self.pk = file_from_test_data['id'] + file_from_test_data = self._get_object_from_test_data("file", requested_index=0) + self.identifier = file_from_test_data["identifier"] + self.pk = file_from_test_data["id"] def test_get_using_dict_with_id(self): - row = {'id': 1, 'other_stuff': 'doesnt matter'} + row = {"id": 1, "other_stuff": "doesnt matter"} try: obj = File.objects.get(using_dict=row) except File.DoesNotExist: @@ -58,11 +58,11 @@ def test_get_using_dict_with_id(self): else: found = True - self.assertEqual(found, True, 'get with using_dict should have returned a result') + self.assertEqual(found, True, "get with using_dict should have returned a result") self.assertEqual(obj.id, 1) def test_get_using_dict_with_identifier(self): - row = {'identifier': self.identifier, 'other_stuff': 'doesnt matter'} + row = {"identifier": self.identifier, "other_stuff": "doesnt matter"} try: obj = File.objects.get(using_dict=row) except File.DoesNotExist: @@ -70,11 +70,11 @@ def test_get_using_dict_with_identifier(self): else: found = True - self.assertEqual(found, True, 'get with using_dict should have returned a result') + self.assertEqual(found, True, "get with using_dict should have returned a result") self.assertEqual(obj.id, 1) def test_get_using_dict_error_not_found_1(self): - row = {'id': 101010, 'other_stuff': 'doesnt matter'} + row = {"id": 101010, "other_stuff": "doesnt matter"} try: File.objects.get(using_dict=row) except File.DoesNotExist: @@ -82,11 +82,15 @@ def test_get_using_dict_error_not_found_1(self): else: found = True - self.assertEqual(found, False, 'get with using_dict should have not returned a result') + self.assertEqual(found, False, "get with using_dict should have not returned a result") def test_get_using_dict_error_preferred_identifier_not_allowed(self): - row = {'research_dataset': {'preferred_identifier': 'urn:nbn:fi:att:e1a2a565-f4e5-4b55-bc92-790482e69845'}, - 'other_stuff': 'doesnt matter'} + row = { + "research_dataset": { + "preferred_identifier": "urn:nbn:fi:att:e1a2a565-f4e5-4b55-bc92-790482e69845" + }, + "other_stuff": "doesnt matter", + } try: File.objects.get(using_dict=row) except ValidationError: @@ -94,11 +98,14 @@ def test_get_using_dict_error_preferred_identifier_not_allowed(self): else: found = True - self.assertEqual(found, False, - 'get with using_dict should have not returned a result, because preferred_identifier was used') + self.assertEqual( + found, + False, + "get with using_dict should have not returned a result, because preferred_identifier was used", + ) def test_get_using_dict_error_identifier_field_missing(self): - row = {'somefield': 111, 'other_stuff': 'doesnt matter'} + row = {"somefield": 111, "other_stuff": "doesnt matter"} try: File.objects.get(using_dict=row) except ValidationError: @@ -106,5 +113,8 @@ def test_get_using_dict_error_identifier_field_missing(self): else: found = True - self.assertEqual(found, False, - 'get with using_dict should have not returned a result because an identifier field is missing') + self.assertEqual( + found, + False, + "get with using_dict should have not returned a result because an identifier field is missing", + ) diff --git a/src/metax_api/tests/models/signals.py b/src/metax_api/tests/models/signals.py new file mode 100644 index 00000000..548474ce --- /dev/null +++ b/src/metax_api/tests/models/signals.py @@ -0,0 +1,46 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT + +from datetime import date + +from django.core.management import call_command +from django.test import TestCase + +from metax_api.models import CatalogRecord, CatalogRecordV2, DeletedObject, Directory, File +from metax_api.tests.utils import TestClassUtils, test_data_file_path + + +class SignalTests(TestCase, TestClassUtils): + + def setUp(self): + call_command("loaddata", test_data_file_path, verbosity=0) + self.today = date.today().strftime("%d/%m/%Y") + + def test_deleting_catalog_record_creates_new_deleted_object(self): + # test that deleting CatalogRecord object creates a new deleted object + CatalogRecord.objects_unfiltered.get(pk=1).delete(hard=True) + deleted_object = DeletedObject.objects.last() + self.assertEqual(deleted_object.model_name, "CatalogRecord") + self.assertEqual(deleted_object.date_deleted.strftime("%d/%m/%Y"), self.today) + + # test that deleting CatalogRecordV2 object creates a new deleted object + CatalogRecordV2.objects_unfiltered.get(pk=2).delete(hard=True) + deleted_object_v2 = DeletedObject.objects.last() + self.assertEqual(deleted_object_v2.model_name, "CatalogRecordV2") + self.assertEqual(deleted_object_v2.date_deleted.strftime("%d/%m/%Y"), self.today) + + def test_deleting_file_creates_new_deleted_object(self): + File.objects.get(pk=1).delete(hard=True) + deleted_object = DeletedObject.objects.last() + self.assertEqual(deleted_object.model_name, "File") + self.assertEqual(deleted_object.date_deleted.strftime("%d/%m/%Y"), self.today) + + def test_deleting_directory_creates_new_deleted_object(self): + Directory.objects.get(pk=1).delete() + deleted_object = DeletedObject.objects.last() + self.assertEqual(deleted_object.model_name, "Directory") + self.assertEqual(deleted_object.date_deleted.strftime("%d/%m/%Y"), self.today) \ No newline at end of file diff --git a/src/metax_api/tests/rabbitmq/consume.py b/src/metax_api/tests/rabbitmq/consume.py index 5837637b..c8eff3f5 100755 --- a/src/metax_api/tests/rabbitmq/consume.py +++ b/src/metax_api/tests/rabbitmq/consume.py @@ -14,26 +14,21 @@ script to listen for messages sent when someone accesses /rest/datasets/pid/rabbitmq """ -test_user = { - 'name': 'testaaja', - 'password': 'testaaja', - 'vhost': 'metax' -} +test_user = {"name": "testaaja", "password": "testaaja", "vhost": "metax"} -credentials = pika.PlainCredentials(test_user['name'], test_user['password']) +credentials = pika.PlainCredentials(test_user["name"], test_user["password"]) connection = pika.BlockingConnection( pika.ConnectionParameters( - settings['HOSTS'][0], - settings['PORT'], - test_user['vhost'], - credentials)) + settings["HOSTS"][0], settings["PORT"], test_user["vhost"], credentials + ) +) channel = connection.channel() -exchange = 'datasets' -queue_1 = 'testaaja-create' -queue_2 = 'testaaja-update' -queue_3 = 'testaaja-delete' +exchange = "datasets" +queue_1 = "testaaja-create" +queue_2 = "testaaja-update" +queue_3 = "testaaja-delete" # note: requires write permission to exchanges # channel.exchange_declare(exchange=exchange, type='fanout') @@ -41,9 +36,9 @@ channel.queue_declare(queue_2, durable=True) channel.queue_declare(queue_3, durable=True) -channel.queue_bind(exchange=exchange, queue=queue_1, routing_key='create') -channel.queue_bind(exchange=exchange, queue=queue_2, routing_key='update') -channel.queue_bind(exchange=exchange, queue=queue_3, routing_key='delete') +channel.queue_bind(exchange=exchange, queue=queue_1, routing_key="create") +channel.queue_bind(exchange=exchange, queue=queue_2, routing_key="update") +channel.queue_bind(exchange=exchange, queue=queue_3, routing_key="delete") def callback_1(ch, method, properties, body): @@ -62,5 +57,5 @@ def callback_3(ch, method, properties, body): channel.basic_consume(queue_2, callback_2, auto_ack=True) channel.basic_consume(queue_3, callback_3, auto_ack=True) -print('[*] Waiting for logs. To exit press CTRL+C') +print("[*] Waiting for logs. To exit press CTRL+C") channel.start_consuming() diff --git a/src/metax_api/tests/services/reference_data_mixin.py b/src/metax_api/tests/services/reference_data_mixin.py index 4d1463f3..9e522a3c 100755 --- a/src/metax_api/tests/services/reference_data_mixin.py +++ b/src/metax_api/tests/services/reference_data_mixin.py @@ -33,7 +33,6 @@ def get(self, *args, **kwargs): class ReferenceDataMixinTests(TestCase, TestClassUtils): - @classmethod def setUpClass(cls): """ @@ -47,7 +46,7 @@ def setUpClass(cls): cls.cache = RedisClient() def setUp(self): - self.cache.delete('reference_data') + self.cache.delete("reference_data") RDM.process_cached_reference_data = None def tearDown(self): @@ -63,14 +62,14 @@ def test_reference_data_reload_ok(self): RDM.get_reference_data(self.cache) self._assert_reference_data_ok() - @patch('metax_api.utils.ReferenceDataLoader.populate_cache_reference_data') + @patch("metax_api.utils.ReferenceDataLoader.populate_cache_reference_data") def test_reference_data_reload_in_progress(self, mock_populate_cache_reference_data): """ Ensure the reference data fetch survives when another request has already started reloading the reference data. The method should retry for a few seconds, and finally succeed """ return_data_after_retries = 1 - mock_populate_cache_reference_data.return_value = 'reload_started_by_other' + mock_populate_cache_reference_data.return_value = "reload_started_by_other" self._populate_cache_reference_data() mock_cache = MockRedisCacheService(return_data_after_retries=return_data_after_retries) @@ -78,10 +77,10 @@ def test_reference_data_reload_in_progress(self, mock_populate_cache_reference_d RDM.get_reference_data(mock_cache) self._assert_reference_data_ok() - #self.assertEqual(mock_cache.call_count, return_data_after_retries, + # self.assertEqual(mock_cache.call_count, return_data_after_retries, # 'ref data fetching should have retried a few times before succeeding') - @patch('metax_api.utils.ReferenceDataLoader.populate_cache_reference_data') + @patch("metax_api.utils.ReferenceDataLoader.populate_cache_reference_data") def test_reference_data_reload_in_progress_times_out(self, mock_populate_cache_reference_data): """ Ensure the reference data fetch finally gives up when another request has already started @@ -91,7 +90,7 @@ def test_reference_data_reload_in_progress_times_out(self, mock_populate_cache_r # since get_reference_data() retries MAX_RETRIES times, it should give up return_data_after_retries = 100 - mock_populate_cache_reference_data.return_value = 'reload_started_by_other' + mock_populate_cache_reference_data.return_value = "reload_started_by_other" self._populate_cache_reference_data() mock_cache = MockRedisCacheService(return_data_after_retries=return_data_after_retries) @@ -99,11 +98,15 @@ def test_reference_data_reload_in_progress_times_out(self, mock_populate_cache_r # the method being tested RDM.get_reference_data(mock_cache) except Exception as e: - self.assertEqual(e.__class__.__name__, 'Http503', 'ref data reload should raise Http503 when it gives up') + self.assertEqual( + e.__class__.__name__, + "Http503", + "ref data reload should raise Http503 when it gives up", + ) self._assert_reference_data_ok() - @patch('metax_api.utils.ReferenceDataLoader.populate_cache_reference_data') + @patch("metax_api.utils.ReferenceDataLoader.populate_cache_reference_data") def test_reference_data_reload_failed(self, mock_populate_cache_reference_data): """ Ensure 503 is raised when reload by the request failed @@ -123,13 +126,17 @@ def test_reference_data_reload_failed(self, mock_populate_cache_reference_data): # the method being tested RDM.get_reference_data(mock_cache) except Exception as e: - self.assertEqual(e.__class__.__name__, 'Http503', 'ref data reload should raise Http503 when it gives up') + self.assertEqual( + e.__class__.__name__, + "Http503", + "ref data reload should raise Http503 when it gives up", + ) self._assert_reference_data_ok() def _assert_reference_data_ok(self): - self.assertEqual('reference_data' in self.cache.get('reference_data'), True) - self.assertEqual('organization_data' in self.cache.get('reference_data'), True) + self.assertEqual("reference_data" in self.cache.get("reference_data"), True) + self.assertEqual("organization_data" in self.cache.get("reference_data"), True) def _populate_cache_reference_data(self): """ @@ -137,7 +144,10 @@ def _populate_cache_reference_data(self): in the tests. Instead, this method is executed to load something in the cache, which get_reference_data() will then try to return """ - self.cache.set('reference_data', { - 'reference_data': {'language': ['stuff']}, - 'organization_data': {'organization': ['stuff']}, - }) + self.cache.set( + "reference_data", + { + "reference_data": {"language": ["stuff"]}, + "organization_data": {"organization": ["stuff"]}, + }, + ) diff --git a/src/metax_api/tests/testdata/catalog_record_test_data_template.json b/src/metax_api/tests/testdata/catalog_record_test_data_template.json index 47038a92..2c518cfd 100755 --- a/src/metax_api/tests/testdata/catalog_record_test_data_template.json +++ b/src/metax_api/tests/testdata/catalog_record_test_data_template.json @@ -40,7 +40,14 @@ } }, "contributor_role": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role" }] } ], @@ -53,18 +60,37 @@ "fi": "Organisaatio" }, "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup", + "pref_label": { + "en": "ResearchGroup", + "fi": "Tutkimusryhmä", + "sv": "Forskningsgrupp", + "und": "Tutkimusryhmä" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] } ], "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "sv": "engelska", + "fi": "englanti", + "und": "englanti" + } } ], "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "pref_label": { + "fi": "Avoin", + "en": "Open", + "und": "Avoin" + } } } }, diff --git a/src/metax_api/tests/testdata/catalog_record_test_data_template_full_att.json b/src/metax_api/tests/testdata/catalog_record_test_data_template_full_att.json index 96d833c2..c2faf66b 100755 --- a/src/metax_api/tests/testdata/catalog_record_test_data_template_full_att.json +++ b/src/metax_api/tests/testdata/catalog_record_test_data_template_full_att.json @@ -28,7 +28,13 @@ "value": 0.111, "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "sv": "engelska", + "fi": "englanti", + "und": "englanti" + } } ], "temporal": [ @@ -49,7 +55,14 @@ "POLYGON((0 0, 0 20, 40 20, 40 0, 0 0))" ], "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + }, + "in_scheme": "http://www.yso.fi/onto/yso/places" } }, { @@ -57,13 +70,26 @@ "alt": "60", "full_address": "The complete address written as a string, with or without formatting", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + }, + "in_scheme": "http://www.yso.fi/onto/yso/places" } } ], "infrastructure": [ { - "identifier": "urn-nbn-fi-research-infras-2016072530" + "identifier": "urn-nbn-fi-research-infras-2016072530", + "pref_label": { + "fi": "Jyväskylän yliopiston fysiikan laitoksen Kiihdytinlaboratorio", + "en": "Accelerator laboratory of the university of Jyväskylä", + "und": "Jyväskylän yliopiston fysiikan laitoksen Kiihdytinlaboratorio" + }, + "in_scheme": "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures" } ], "access_rights": { @@ -72,28 +98,35 @@ }, "available": "2014-01-15", "access_type": { + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { - "en": "pref label for this type" - }, - "definition": { - "en": "A statement or formal explanation of the meaning of a concept." - }, - "in_scheme": "http://uri.of.filetype.concept/scheme" + "fi": "Avoin", + "en": "Open", + "und": "Avoin" + } }, "restriction_grounds": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other" + "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other", + "pref_label": { + "fi": "Saatavuutta rajoitettu muulla perusteella", + "en": "Restricted access due to other reasons", + "sv": "Begränsad åtkomst av övriga skäl", + "und": "Saatavuutta rajoitettu muulla perusteella" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds" }], "license": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" }, + "license": "https://url.of.license.which.applies.here.org", + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "description": { "en": "Free account of the rights" - }, - "license": "https://url.of.license.which.applies.here.org" + } } ], "access_url": { @@ -122,7 +155,14 @@ "+358501231235" ], "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson", + "pref_label": { + "en": "Point of contact", + "fi": "Yhteystiedot", + "sv": "Kontaktuppgifter", + "und": "Yhteystiedot" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] }, "type": { @@ -151,7 +191,14 @@ "+358501231235" ], "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector", + "pref_label": { + "en": "Data Collector", + "fi": "Aineiston kerääjä", + "sv": "Datainsamling", + "und": "Aineiston kerääjä" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] }, "type": { @@ -207,15 +254,27 @@ "alt": "11.111", "full_address": "The complete address written as a string, with or without formatting", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + }, + "in_scheme": "http://www.yso.fi/onto/yso/places" } }, "lifecycle_event": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event", "definition": { "en": "A statement or formal explanation of the meaning of a concept." }, - "in_scheme": "http://uri.of.filetype.concept/scheme" + "identifier": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", + "pref_label": { + "fi": "Kerätty", + "en": "Collected", + "und": "Kerätty" + } }, "used_entity": [ { @@ -251,7 +310,14 @@ "+358501231235" ], "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator", + "pref_label": { + "en": "Data Curator", + "fi": "Aineiston käsittelijä", + "sv": "Databehandling", + "und": "Aineiston käsittelijä" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] } ], @@ -287,7 +353,14 @@ } ], "event_outcome": { - "identifier": "Success" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/success", + "pref_label": { + "en": "Success", + "fi": "Onnistunut", + "sv": "Framgångsrik", + "und": "Onnistunut" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome" }, "outcome_description": {"fi": "Onnistui hyvin", "en": "A grand success"} }, @@ -307,14 +380,34 @@ "alt": "11.111", "full_address": "The complete address written as a string, with or without formatting", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p105917" + "identifier": "http://www.yso.fi/onto/yso/p105917", + "pref_label": { + "fi": "Etelä-Suomi", + "sv": "Södra Finland", + "en": "Southern Finland Province", + "und": "Etelä-Suomi" + }, + "in_scheme": "http://www.yso.fi/onto/yso/places" } }, "preservation_event": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd" + "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd", + "pref_label": { + "fi": "Objektin päivitys", + "en": "Object update", + "und": "Objektin päivitys" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/preservation_event" }, "event_outcome": { - "identifier": "Failure" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/failure", + "pref_label": { + "en": "Failure", + "fi": "Epäonnistunut", + "sv": "Misslyckad", + "und": "Epäonnistunut" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome" }, "outcome_description": {"fi": "Epäonnistui", "en": "A grand failure"} } @@ -339,7 +432,14 @@ } }, "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager", + "pref_label": { + "en": "Data Manager", + "fi": "Aineiston hallinnoija", + "sv": "Dataadministration", + "und": "Aineiston hallinnoija" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] }], "creator": [ @@ -355,7 +455,14 @@ } }, "contributor_role": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization", + "pref_label": { + "en": "Conceptualization", + "fi": "Tutkimuksen muotoilu", + "sv": "Formulering av forskningen", + "und": "Tutkimuksen muotoilu" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role" }] } ], @@ -376,7 +483,14 @@ } }, "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distributör", + "und": "Jakelija" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] } ], @@ -418,7 +532,14 @@ } }, "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distributör", + "und": "Jakelija" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] }, "contributor": [ @@ -443,7 +564,14 @@ ] }, "contributor_role": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition", + "pref_label": { + "en": "Funding acquisition", + "fi": "Rahoituksen hankinta", + "sv": "Anskaffande av finansiering", + "und": "Rahoituksen hankinta" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role" }] }, { @@ -467,7 +595,14 @@ ] }, "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader", + "pref_label": { + "en": "Project Leader", + "fi": "Projektin johtaja", + "sv": "Projektledare", + "und": "Projektin johtaja" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] } ], @@ -491,7 +626,14 @@ "+358501232233" ], "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor", + "pref_label": { + "en": "Editor", + "fi": "Julkaisutoimittaja", + "sv": "Redaktör", + "und": "Julkaisutoimittaja" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] } ], @@ -508,12 +650,25 @@ "+358501231235" ], "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution", + "pref_label": { + "en": "Hosting Institution", + "fi": "Säilyttävä organisaatio", + "sv": "Bevarande organisation", + "und": "Säilyttävä organisaatio" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] } ], "funder_type": { - "identifier": "tekes" + "identifier": "http://uri.suomi.fi/codelist/fairdata/funder_type/code/tekes", + "pref_label": { + "fi": "Tekes", + "en": "Tekes", + "und": "Tekes" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/funder_type" } } ], @@ -539,14 +694,15 @@ }, "license": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" }, + "license": "https://url.of.license.which.applies.here.org", + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "description": { "en": "Free account of the rights" - }, - "license": "https://url.of.license.which.applies.org" + } } ], "file_type": { @@ -588,14 +744,15 @@ }, "license": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" }, + "license": "https://url.of.license.which.applies.here.org", + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "description": { "en": "Free account of the rights" - }, - "license": "https://url.of.license.which.applies.org" + } } ], "resource_type": { @@ -623,11 +780,22 @@ }, "identifier": "external:dataset:identifier", "type": { - "identifier": "physical_object" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object", + "pref_label": { + "fi": "Fyysinen objekti", + "en": "Physical object", + "und": "Fyysinen objekti" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type" } }, "relation_type": { - "identifier": "wasDerivedFrom" + "identifier": "http://www.w3.org/ns/prov#wasDerivedFrom", + "pref_label": { + "fi": "Johdettu aineistosta", + "en": "Was derived from", + "und": "Johdettu aineistosta" + } } }, { @@ -641,11 +809,22 @@ }, "identifier": "external:thing:identifier", "type": { - "identifier": "collection" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection", + "pref_label": { + "fi": "Kokoelma", + "en": "Collection", + "und": "Kokoelma" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type" } }, "relation_type": { - "identifier": "cites" + "identifier": "http://purl.org/spar/cito/cites", + "pref_label": { + "fi": "Viittaa", + "en": "Cites", + "und": "Viittaa" + } } }, { @@ -659,11 +838,22 @@ }, "identifier": "external:dataset_2:identifier", "type": { - "identifier": "service" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service", + "pref_label": { + "fi": "Palvelu", + "en": "Service", + "und": "Palvelu" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type" } }, "relation_type": { - "identifier": "isPartOf" + "identifier": "http://purl.org/dc/terms/isPartOf", + "pref_label": { + "fi": "Kuuluu aineistoon", + "en": "Is part of", + "und": "Kuuluu aineistoon" + } } }, { @@ -677,11 +867,22 @@ }, "identifier": "external:thing_2:identifier", "type": { - "identifier": "ui" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui", + "pref_label": { + "fi": "Käyttöliittymä", + "en": "User interface", + "und": "Käyttöliittymä" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type" } }, "relation_type": { - "identifier": "hasPreviousVersion" + "identifier": "http://www.w3.org/ns/adms#previous", + "pref_label": { + "fi": "Edellinen versio", + "en": "Has previous version", + "und": "Edellinen versio" + } } } ] diff --git a/src/metax_api/tests/testdata/catalog_record_test_data_template_full_ida.json b/src/metax_api/tests/testdata/catalog_record_test_data_template_full_ida.json index 8fc1fd54..b3f3ef82 100755 --- a/src/metax_api/tests/testdata/catalog_record_test_data_template_full_ida.json +++ b/src/metax_api/tests/testdata/catalog_record_test_data_template_full_ida.json @@ -28,7 +28,13 @@ "value": 0.111, "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "sv": "engelska", + "fi": "englanti", + "und": "englanti" + } } ], "temporal": [ @@ -49,7 +55,14 @@ "POLYGON((0 0, 0 20, 40 20, 40 0, 0 0))" ], "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + }, + "in_scheme": "http://www.yso.fi/onto/yso/places" } }, { @@ -57,13 +70,26 @@ "alt": "60", "full_address": "The complete address written as a string, with or without formatting", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + }, + "in_scheme": "http://www.yso.fi/onto/yso/places" } } ], "infrastructure": [ { - "identifier": "urn-nbn-fi-research-infras-2016072530" + "identifier": "urn-nbn-fi-research-infras-2016072530", + "pref_label": { + "fi": "Jyväskylän yliopiston fysiikan laitoksen Kiihdytinlaboratorio", + "en": "Accelerator laboratory of the university of Jyväskylä", + "und": "Jyväskylän yliopiston fysiikan laitoksen Kiihdytinlaboratorio" + }, + "in_scheme": "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures" } ], "access_rights": { @@ -72,43 +98,55 @@ }, "available": "2014-01-15", "access_type": { + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { - "en": "pref label for this type" - }, - "definition": { - "en": "A statement or formal explanation of the meaning of a concept." - }, - "in_scheme": "http://uri.of.filetype.concept/scheme" + "fi": "Avoin", + "en": "Open", + "und": "Avoin" + } }, "restriction_grounds": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other" + "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other", + "pref_label": { + "fi": "Saatavuutta rajoitettu muulla perusteella", + "en": "Restricted access due to other reasons", + "sv": "Begränsad åtkomst av övriga skäl", + "und": "Saatavuutta rajoitettu muulla perusteella" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds" }], "license": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" }, + "license": "https://url.of.license.which.applies.here.org", + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "description": { "en": "Free account of the rights" - }, - "license": "https://url.of.license.which.applies.here.org" + } }, { - "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-2.0", "title": { - "en": "A name given to the resource" + "fi": "Creative Commons Nimeä-EiKaupallinen 2.0 Yleinen (CC BY-NC 2.0)", + "en": "Creative Commons Attribution-NonCommercial 2.0 Generic (CC BY-NC 2.0", + "und": "Creative Commons Nimeä-EiKaupallinen 2.0 Yleinen (CC BY-NC 2.0)" }, + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-2.0", "description": { "en": "Free account of the rights" - } + }, + "license": "https://creativecommons.org/licenses/by-nc/2.0/" }, { - "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/other", "title": { - "en": "A name given to the resource" - } + "fi": "Muu", + "en": "Other", + "und": "Muu" + }, + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/other" } ], "access_url": { @@ -137,7 +175,14 @@ "+358501231235" ], "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson", + "pref_label": { + "en": "Point of contact", + "fi": "Yhteystiedot", + "sv": "Kontaktuppgifter", + "und": "Yhteystiedot" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] }, "type": { @@ -166,7 +211,14 @@ "+358501231235" ], "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector", + "pref_label": { + "en": "Data Collector", + "fi": "Aineiston kerääjä", + "sv": "Datainsamling", + "und": "Aineiston kerääjä" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] }, "type": { @@ -232,15 +284,27 @@ "alt": "11.111", "full_address": "The complete address written as a string, with or without formatting", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + }, + "in_scheme": "http://www.yso.fi/onto/yso/places" } }, "lifecycle_event": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event", "definition": { "en": "A statement or formal explanation of the meaning of a concept." }, - "in_scheme": "http://uri.of.filetype.concept/scheme" + "identifier": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", + "pref_label": { + "fi": "Kerätty", + "en": "Collected", + "und": "Kerätty" + } }, "used_entity": [ { @@ -276,7 +340,14 @@ "+358501231235" ], "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator", + "pref_label": { + "en": "Data Curator", + "fi": "Aineiston käsittelijä", + "sv": "Databehandling", + "und": "Aineiston käsittelijä" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] } ], @@ -312,7 +383,14 @@ } ], "event_outcome": { - "identifier": "Success" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/success", + "pref_label": { + "en": "Success", + "fi": "Onnistunut", + "sv": "Framgångsrik", + "und": "Onnistunut" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome" }, "outcome_description": {"fi": "Onnistui hyvin", "en": "A grand success"} }, @@ -332,14 +410,34 @@ "alt": "11.111", "full_address": "The complete address written as a string, with or without formatting", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p105917" + "identifier": "http://www.yso.fi/onto/yso/p105917", + "pref_label": { + "fi": "Etelä-Suomi", + "sv": "Södra Finland", + "en": "Southern Finland Province", + "und": "Etelä-Suomi" + }, + "in_scheme": "http://www.yso.fi/onto/yso/places" } }, "preservation_event": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd" + "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd", + "pref_label": { + "fi": "Objektin päivitys", + "en": "Object update", + "und": "Objektin päivitys" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/preservation_event" }, "event_outcome": { - "identifier": "Failure" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/failure", + "pref_label": { + "en": "Failure", + "fi": "Epäonnistunut", + "sv": "Misslyckad", + "und": "Epäonnistunut" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome" }, "outcome_description": {"fi": "Epäonnistui", "en": "A grand failure"} } @@ -364,7 +462,14 @@ } }, "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager", + "pref_label": { + "en": "Data Manager", + "fi": "Aineiston hallinnoija", + "sv": "Dataadministration", + "und": "Aineiston hallinnoija" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] }], "creator": [ @@ -380,7 +485,14 @@ } }, "contributor_role": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization", + "pref_label": { + "en": "Conceptualization", + "fi": "Tutkimuksen muotoilu", + "sv": "Formulering av forskningen", + "und": "Tutkimuksen muotoilu" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role" }] } ], @@ -402,10 +514,24 @@ }, "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distributör", + "und": "Jakelija" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }, { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Sponsor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Sponsor", + "pref_label": { + "en": "Sponsor", + "fi": "Sponsori", + "sv": "Sponsor", + "und": "Sponsori" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" } ] } @@ -448,7 +574,14 @@ } }, "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distributör", + "und": "Jakelija" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] }, "contributor": [ @@ -473,7 +606,14 @@ ] }, "contributor_role": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition", + "pref_label": { + "en": "Funding acquisition", + "fi": "Rahoituksen hankinta", + "sv": "Anskaffande av finansiering", + "und": "Rahoituksen hankinta" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role" }] }, { @@ -497,7 +637,14 @@ ] }, "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader", + "pref_label": { + "en": "Project Leader", + "fi": "Projektin johtaja", + "sv": "Projektledare", + "und": "Projektin johtaja" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] } ], @@ -521,7 +668,14 @@ "+358501232233" ], "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor", + "pref_label": { + "en": "Editor", + "fi": "Julkaisutoimittaja", + "sv": "Redaktör", + "und": "Julkaisutoimittaja" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] } ], @@ -538,12 +692,25 @@ "+358501231235" ], "contributor_type": [{ - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution", + "pref_label": { + "en": "Hosting Institution", + "fi": "Säilyttävä organisaatio", + "sv": "Bevarande organisation", + "und": "Säilyttävä organisaatio" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type" }] } ], "funder_type": { - "identifier": "tekes" + "identifier": "http://uri.suomi.fi/codelist/fairdata/funder_type/code/tekes", + "pref_label": { + "fi": "Tekes", + "en": "Tekes", + "und": "Tekes" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/funder_type" } } ], @@ -609,11 +776,22 @@ }, "identifier": "external:dataset:identifier", "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object", + "pref_label": { + "fi": "Fyysinen objekti", + "en": "Physical object", + "und": "Fyysinen objekti" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type" } }, "relation_type": { - "identifier": "wasDerivedFrom" + "identifier": "http://www.w3.org/ns/prov#wasDerivedFrom", + "pref_label": { + "fi": "Johdettu aineistosta", + "en": "Was derived from", + "und": "Johdettu aineistosta" + } } }, { @@ -627,11 +805,22 @@ }, "identifier": "external:thing:identifier", "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection", + "pref_label": { + "fi": "Kokoelma", + "en": "Collection", + "und": "Kokoelma" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type" } }, "relation_type": { - "identifier": "cites" + "identifier": "http://purl.org/spar/cito/cites", + "pref_label": { + "fi": "Viittaa", + "en": "Cites", + "und": "Viittaa" + } } }, { @@ -645,11 +834,22 @@ }, "identifier": "external:dataset_2:identifier", "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service", + "pref_label": { + "fi": "Palvelu", + "en": "Service", + "und": "Palvelu" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type" } }, "relation_type": { - "identifier": "isPartOf" + "identifier": "http://purl.org/dc/terms/isPartOf", + "pref_label": { + "fi": "Kuuluu aineistoon", + "en": "Is part of", + "und": "Kuuluu aineistoon" + } } }, { @@ -663,11 +863,22 @@ }, "identifier": "external:thing_2:identifier", "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui", + "pref_label": { + "fi": "Käyttöliittymä", + "en": "User interface", + "und": "Käyttöliittymä" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type" } }, "relation_type": { - "identifier": "hasPreviousVersion" + "identifier": "http://www.w3.org/ns/adms#previous", + "pref_label": { + "fi": "Edellinen versio", + "en": "Has previous version", + "und": "Edellinen versio" + } } } ] diff --git a/src/metax_api/tests/testdata/data_catalog_test_data_template.json b/src/metax_api/tests/testdata/data_catalog_test_data_template.json index 391c33fe..decbf52a 100755 --- a/src/metax_api/tests/testdata/data_catalog_test_data_template.json +++ b/src/metax_api/tests/testdata/data_catalog_test_data_template.json @@ -9,10 +9,22 @@ "harvested": false, "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/fin" + "identifier": "http://lexvo.org/id/iso639-3/fin", + "title": { + "sv": "finska", + "en": "Finnish", + "fi": "suomi", + "und": "suomi" + } }, { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "sv": "engelska", + "fi": "englanti", + "und": "englanti" + } } ], "homepage": [ @@ -76,7 +88,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { "fi": "Avoin", - "en": "Open" + "en": "Open", + "und": "Avoin" } } ], @@ -84,8 +97,9 @@ { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0", "title": { - "fi": "CC BY 4.0", - "en": "CC BY 4.0" + "fi": "Creative Commons Nimeä 4.0 Kansainvälinen (CC BY 4.0)", + "en": "Creative Commons Attribution 4.0 International (CC BY 4.0)", + "und": "Creative Commons Nimeä 4.0 Kansainvälinen (CC BY 4.0)" } } ] diff --git a/src/metax_api/tests/testdata/fetch_and_update_datasets.py b/src/metax_api/tests/testdata/fetch_and_update_datasets.py index 084dac6f..d8597b91 100755 --- a/src/metax_api/tests/testdata/fetch_and_update_datasets.py +++ b/src/metax_api/tests/testdata/fetch_and_update_datasets.py @@ -33,26 +33,32 @@ def get_auth_header(): for u in settings.API_USERS: - if u['username'] == 'metax': + if u["username"] == "metax": return { - 'Authorization': 'Basic %s' - % b64encode(bytes('%s:%s' % (u['username'], u['password']), 'utf-8')).decode('utf-8') + "Authorization": "Basic %s" + % b64encode(bytes("%s:%s" % (u["username"], u["password"]), "utf-8")).decode( + "utf-8" + ) } + def get_test_datasets(): - print('retrieving test datasets...') - response = requests.get('https://localhost/rest/datasets?metadata_owner_org=abc-org-123&pagination=false', - headers=headers, verify=False) + print("retrieving test datasets...") + response = requests.get( + "https://localhost/rest/datasets?metadata_owner_org=abc-org-123&pagination=false", + headers=headers, + verify=False, + ) if response.status_code != 200: raise Exception(response.content) records = response.json() if not records: - print('Received no records. Quiting...') + print("Received no records. Quiting...") return - print('Filtering out possible non-test datasets...') + print("Filtering out possible non-test datasets...") test_records = [] for record in records: if dataset_is_testdata(record): @@ -60,60 +66,81 @@ def get_test_datasets(): return test_records + def dataset_is_testdata(record): # Test datasets have fixed attributes so these should verify that it is a test dataset # Other tests could be added but these should be sufficient - end = f'd{record["id"]}' if record['id'] < 10 else f'{record["id"]}' + end = f'd{record["id"]}' if record["id"] < 10 else f'{record["id"]}' - if record['identifier'] != f'cr955e904-e3dd-4d7e-99f1-3fed446f96{end}' or \ - record['research_dataset']['creator'][0]['name'] != 'Teppo Testaaja': + if ( + record["identifier"] != f"cr955e904-e3dd-4d7e-99f1-3fed446f96{end}" + or record["research_dataset"]["creator"][0]["name"] != "Teppo Testaaja" + ): return False return True + def retrieve_and_update_all_datasets_in_db(headers): - print('-- begin retrieving and updating test datasets --') + print("-- begin retrieving and updating test datasets --") test_records = get_test_datasets() # dont want to create new versions from datasets for this operation, # so use parameter preserve_version - print('updating the test datasets...') - response = requests.put('https://localhost/rest/datasets?preserve_version', - headers=headers, data=dumps(test_records), verify=False) + print("updating the test datasets...") + response = requests.put( + "https://localhost/rest/datasets?preserve_version", + headers=headers, + data=dumps(test_records), + verify=False, + ) if response.status_code not in (200, 201, 204): print(response.status_code) raise Exception(response.text) - elif response.text and len(response.json().get('failed', [])) > 0: - for fail in response.json().get('failed'): + elif response.text and len(response.json().get("failed", [])) > 0: + for fail in response.json().get("failed"): raise Exception(fail) - print('-- done --') + print("-- done --") def retrieve_and_update_all_data_catalogs_in_db(headers): - print('-- begin retrieving and updating all data catalogs in the db --') + print("-- begin retrieving and updating all data catalogs in the db --") - print('retrieving all data catalog IDs...') - response = requests.get('https://localhost/rest/datacatalogs?limit=100', headers=headers, verify=False) + print("retrieving all data catalog IDs...") + response = requests.get( + "https://localhost/rest/datacatalogs?limit=100", headers=headers, verify=False + ) if response.status_code != 200: raise Exception(response.content) data_catalog_ids = [] - for dc in response.json().get('results', []): - if dc['catalog_json']['identifier'] == 'urn:nbn:fi:att:data-catalog-dft': + for dc in response.json().get("results", []): + if dc["catalog_json"]["identifier"] == "urn:nbn:fi:att:data-catalog-dft": pass else: - data_catalog_ids.append(dc.get('id')) + data_catalog_ids.append(dc.get("id")) - print('retrieving details of data catalogs and updating %d data catalogs...' % len(data_catalog_ids)) + print( + "retrieving details of data catalogs and updating %d data catalogs..." + % len(data_catalog_ids) + ) for dc_id in data_catalog_ids: - response = requests.get('https://localhost/rest/datacatalogs/%s' % dc_id, headers=headers, verify=False) + response = requests.get( + "https://localhost/rest/datacatalogs/%s" % dc_id, + headers=headers, + verify=False, + ) if response.status_code == 200: - update_response = requests.put('https://localhost/rest/datacatalogs/%s' % dc_id, - headers=headers, json=response.json(), verify=False) + update_response = requests.put( + "https://localhost/rest/datacatalogs/%s" % dc_id, + headers=headers, + json=response.json(), + verify=False, + ) if update_response.status_code not in (200, 201, 204): print(response.status_code) raise Exception(response.text) @@ -121,45 +148,54 @@ def retrieve_and_update_all_data_catalogs_in_db(headers): print(response.status_code) raise Exception(response.content) - print('-- done --') + print("-- done --") def update_directory_byte_sizes_and_file_counts(headers): - print('-- begin updating byte sizes and file counts in all dirs in all projects --') - response = requests.get('https://localhost/rest/directories/update_byte_sizes_and_file_counts', - headers=headers, verify=False) + print("-- begin updating byte sizes and file counts in all dirs in all projects --") + response = requests.get( + "https://localhost/rest/directories/update_byte_sizes_and_file_counts", + headers=headers, + verify=False, + ) if response.status_code not in (200, 201, 204): raise Exception(response.text) - print('-- done --') + print("-- done --") def update_ida_datasets_total_files_byte_size(headers): - print('-- begin updating IDA CR total ida byte sizes --') - response = requests.get('https://localhost/rest/datasets/update_cr_total_files_byte_sizes', - headers=headers, verify=False) + print("-- begin updating IDA CR total ida byte sizes --") + response = requests.get( + "https://localhost/rest/datasets/update_cr_total_files_byte_sizes", + headers=headers, + verify=False, + ) if response.status_code not in (200, 201, 204): raise Exception(response.text) - print('-- done --') + print("-- done --") def update_cr_directory_browsing_data(headers): - print('-- begin updating test CR directory byte sizes and file counts --') + print("-- begin updating test CR directory byte sizes and file counts --") test_records = get_test_datasets() for r in test_records: - response = requests.get(f'https://localhost/rest/datasets/update_cr_directory_browsing_data?id={r["id"]}', - headers=headers, verify=False) + response = requests.get( + f'https://localhost/rest/datasets/update_cr_directory_browsing_data?id={r["id"]}', + headers=headers, + verify=False, + ) if response.status_code not in (200, 201, 204): raise Exception(response.text) - print('-- done --') + print("-- done --") -if __name__ == '__main__': - headers = {'Content-type': 'application/json'} +if __name__ == "__main__": + headers = {"Content-type": "application/json"} headers.update(get_auth_header()) for i in range(1, 10): - response = requests.get('https://localhost/rest/datasets/1', headers=headers, verify=False) + response = requests.get("https://localhost/rest/datasets/1", headers=headers, verify=False) if response.status_code == 200: break sleep(1) @@ -167,6 +203,7 @@ def update_cr_directory_browsing_data(headers): print("Unable to GET dataset with pk 1, aborting... reason:") print(response.content) import sys + sys.exit(1) retrieve_and_update_all_datasets_in_db(headers) diff --git a/src/metax_api/tests/testdata/generate_test_data.py b/src/metax_api/tests/testdata/generate_test_data.py index dca29026..6729396a 100755 --- a/src/metax_api/tests/testdata/generate_test_data.py +++ b/src/metax_api/tests/testdata/generate_test_data.py @@ -42,18 +42,18 @@ # spread these evenly among the cr's catalog_records_owner_ids = [ - '053bffbcc41edad4853bea91fc42ea18', - '053d18ecb29e752cb7a35cd77b34f5fd', - '05593961536b76fa825281ccaedd4d4f', - '055ea4dade5ab2145954f56d4b51cef0', - '055ea531a6cac569425bed94459266ee', + "053bffbcc41edad4853bea91fc42ea18", + "053d18ecb29e752cb7a35cd77b34f5fd", + "05593961536b76fa825281ccaedd4d4f", + "055ea4dade5ab2145954f56d4b51cef0", + "055ea531a6cac569425bed94459266ee", ] # very slow with a large number of rows. we'll always validate the first loop tho validate_json = False # Location of schema files -schema_path = os.path.dirname(__file__) + '../api.rest.base/schemas' +schema_path = os.path.dirname(__file__) + "../api.rest.base/schemas" # identifier model type cr_type = 1 # catalog record @@ -61,28 +61,28 @@ def generate_file_storages(file_storage_max_rows): - print('generating file storages...') + print("generating file storages...") test_file_storage_list = [] - with open('file_storage_test_data_template.json') as json_file: + with open("file_storage_test_data_template.json") as json_file: row_template = json_load(json_file) - title = row_template['file_storage_json']['title'] - identifier = "pid:urn:storage" + row_template['file_storage_json']['identifier'] + title = row_template["file_storage_json"]["title"] + identifier = "pid:urn:storage" + row_template["file_storage_json"]["identifier"] for i in range(1, file_storage_max_rows + 1): new = { - 'fields': { - 'date_modified': '2017-06-23T10:07:22Z', - 'date_created': '2017-05-23T10:07:22Z', - 'file_storage_json': { - 'title': title % str(i), - 'identifier': identifier % str(i), - 'url': 'https://metax.fd-test.csc.fi/rest/filestorages/%d' % i, - } + "fields": { + "date_modified": "2017-06-23T10:07:22Z", + "date_created": "2017-05-23T10:07:22Z", + "file_storage_json": { + "title": title % str(i), + "identifier": identifier % str(i), + "url": "https://metax.fd-test.csc.fi/rest/filestorages/%d" % i, + }, }, - 'model': "metax_api.filestorage", - 'pk': i + "model": "metax_api.filestorage", + "pk": i, } test_file_storage_list.append(new) @@ -90,139 +90,169 @@ def generate_file_storages(file_storage_max_rows): def generate_files(test_file_storage_list, validate_json): - print('generating files...') + print("generating files...") - with open('file_test_data_template.json') as json_file: + with open("file_test_data_template.json") as json_file: row_template = json_load(json_file) - json_template = row_template['file_characteristics'].copy() - file_name = row_template['file_name'] - json_title = json_template['title'] - json_description = json_template['description'] + json_template = row_template["file_characteristics"].copy() + file_name = row_template["file_name"] + json_title = json_template["title"] + json_description = json_template["description"] directories = [] file_test_data_list = [] directory_test_data_list = [] - json_schema = get_json_schema('file') - file_storage = test_file_storage_list[0]['pk'] + json_schema = get_json_schema("file") + file_storage = test_file_storage_list[0]["pk"] for i in range(1, file_max_rows + 1): if i <= 20: - project_identifier = 'project_x' - project_root_folder = 'project_x_FROZEN' + project_identifier = "project_x" + project_root_folder = "project_x_FROZEN" else: - project_identifier = 'research_project_112' - project_root_folder = 'prj_112_root' + project_identifier = "research_project_112" + project_root_folder = "prj_112_root" loop = str(i) new = { - 'fields': row_template.copy(), - 'model': 'metax_api.file', + "fields": row_template.copy(), + "model": "metax_api.file", } - file_path = row_template['file_path'] + file_path = row_template["file_path"] # assing files to different directories to have something to browse if 1 <= i < 6: - file_path = file_path.replace('/some/path/', '/{0}/Experiment_X/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", "/{0}/Experiment_X/".format(project_root_folder) + ) elif 6 <= i < 11: - file_path = file_path.replace('/some/path/', '/{0}/Experiment_X/Phase_1/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", "/{0}/Experiment_X/Phase_1/".format(project_root_folder) + ) elif 11 <= i <= 20: - file_path = file_path.replace('/some/path/', '/{0}/Experiment_X/Phase_1/2017/01/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", + "/{0}/Experiment_X/Phase_1/2017/01/".format(project_root_folder), + ) if i == 21: - file_path = file_path.replace('/some/path/', '/{0}/science_data_A/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", "/{0}/science_data_A/".format(project_root_folder) + ) if 22 <= i < 25: - file_path = file_path.replace('/some/path/', '/{0}/science_data_A/phase_1/2018/01/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", + "/{0}/science_data_A/phase_1/2018/01/".format(project_root_folder), + ) if i == 25: - file_path = file_path.replace('/some/path/', '/{0}/science_data_B/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", "/{0}/science_data_B/".format(project_root_folder) + ) if i == 26: - file_path = file_path.replace('/some/path/', '/{0}/other/items/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", "/{0}/other/items/".format(project_root_folder) + ) if 27 <= i < 30: - file_path = file_path.replace('/some/path/', '/{0}/random_folder/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", "/{0}/random_folder/".format(project_root_folder) + ) if 30 <= i < 35: - file_path = file_path.replace('/some/path/', '/{0}/science_data_C/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", "/{0}/science_data_C/".format(project_root_folder) + ) elif 35 <= i < 40: - file_path = file_path.replace('/some/path/', '/{0}/science_data_C/phase_1/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", + "/{0}/science_data_C/phase_1/".format(project_root_folder), + ) elif 40 <= i < 50: - file_path = file_path.replace('/some/path/', '/{0}/science_data_C/phase_1/2017/01/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", + "/{0}/science_data_C/phase_1/2017/01/".format(project_root_folder), + ) elif 50 <= i < 70: - file_path = file_path.replace('/some/path/', '/{0}/science_data_C/phase_1/2017/02/'. - format(project_root_folder)) + file_path = file_path.replace( + "/some/path/", + "/{0}/science_data_C/phase_1/2017/02/".format(project_root_folder), + ) elif 70 <= i <= file_max_rows: - file_path = file_path.replace('/some/path/', '/{0}/science_data_C/phase_2/2017/10/'. - format(project_root_folder)) - - directory_id = get_parent_directory_for_path(directories, file_path, directory_test_data_list, - project_identifier) - - new['fields']['parent_directory'] = directory_id - new['fields']['project_identifier'] = project_identifier - new['fields']['file_name'] = file_name % loop - new['fields']['file_path'] = file_path % loop - new['fields']['identifier'] = "pid:urn:" + loop - new['fields']['file_characteristics']['title'] = json_title % loop - new['fields']['file_characteristics']['description'] = json_description % loop - new['fields']['file_storage'] = file_storage - new['fields']['byte_size'] = i * 100 - new['pk'] = i + file_path = file_path.replace( + "/some/path/", + "/{0}/science_data_C/phase_2/2017/10/".format(project_root_folder), + ) + + directory_id = get_parent_directory_for_path( + directories, file_path, directory_test_data_list, project_identifier + ) + + new["fields"]["parent_directory"] = directory_id + new["fields"]["project_identifier"] = project_identifier + new["fields"]["file_name"] = file_name % loop + new["fields"]["file_path"] = file_path % loop + new["fields"]["identifier"] = "pid:urn:" + loop + new["fields"]["file_characteristics"]["title"] = json_title % loop + new["fields"]["file_characteristics"]["description"] = json_description % loop + new["fields"]["file_storage"] = file_storage + new["fields"]["byte_size"] = i * 100 + new["pk"] = i if validate_json or i == 1: - json_validate(new['fields']['file_characteristics'], json_schema) + json_validate(new["fields"]["file_characteristics"], json_schema) file_test_data_list.append(new) return file_test_data_list, directory_test_data_list -def get_parent_directory_for_path(directories, file_path, directory_test_data_list, project_identifier): +def get_parent_directory_for_path( + directories, file_path, directory_test_data_list, project_identifier +): dir_name = os.path.dirname(file_path) for d in directories: - if d['fields']['directory_path'] == dir_name and d['fields']['project_identifier'] == project_identifier: - return d['pk'] - return create_parent_directory_for_path(directories, dir_name, directory_test_data_list, project_identifier) - - -def create_parent_directory_for_path(directories, file_path, directory_test_data_list, project_identifier): + if ( + d["fields"]["directory_path"] == dir_name + and d["fields"]["project_identifier"] == project_identifier + ): + return d["pk"] + return create_parent_directory_for_path( + directories, dir_name, directory_test_data_list, project_identifier + ) + + +def create_parent_directory_for_path( + directories, file_path, directory_test_data_list, project_identifier +): """ Recursively creates the requested directories for file_path """ - with open('directory_test_data_template.json') as json_file: + with open("directory_test_data_template.json") as json_file: row_template = json_load(json_file) - if file_path == '/': + if file_path == "/": directory_id = None else: # the directory where a file or dir belongs to, must be retrieved or created first - directory_id = get_parent_directory_for_path(directories, file_path, directory_test_data_list, - project_identifier) + directory_id = get_parent_directory_for_path( + directories, file_path, directory_test_data_list, project_identifier + ) # all parent dirs have been created - now create the dir that was originally asked for new_id = len(directories) + 1 new = { - 'fields': row_template.copy(), - 'model': 'metax_api.directory', - 'pk': new_id, + "fields": row_template.copy(), + "model": "metax_api.directory", + "pk": new_id, } # note: it is possible that parent_directory is null (top-level directories) - new['fields']['parent_directory'] = directory_id - new['fields']['directory_name'] = os.path.basename(file_path) - new['fields']['directory_path'] = file_path - new['fields']['identifier'] = new['fields']['identifier'] % new_id - new['fields']['project_identifier'] = project_identifier + new["fields"]["parent_directory"] = directory_id + new["fields"]["directory_name"] = os.path.basename(file_path) + new["fields"]["directory_path"] = file_path + new["fields"]["identifier"] = new["fields"]["identifier"] % new_id + new["fields"]["project_identifier"] = project_identifier directory_test_data_list.append(new) directories.append(new) @@ -230,234 +260,317 @@ def create_parent_directory_for_path(directories, file_path, directory_test_data return new_id -def save_test_data(file_storage_list, file_list, directory_list, data_catalogs_list, contract_list, - catalog_record_list, dataset_version_sets): - with open('test_data.json', 'w') as f: - print('dumping test data as json to metax_api/tests/test_data.json...') - json_dump(file_storage_list + directory_list + file_list + data_catalogs_list + contract_list + - dataset_version_sets + catalog_record_list, f, indent=4, sort_keys=True) +def save_test_data( + file_storage_list, + file_list, + directory_list, + data_catalogs_list, + contract_list, + catalog_record_list, + dataset_version_sets, +): + with open("test_data.json", "w") as f: + print("dumping test data as json to metax_api/tests/test_data.json...") + json_dump( + file_storage_list + + directory_list + + file_list + + data_catalogs_list + + contract_list + + dataset_version_sets + + catalog_record_list, + f, + indent=4, + sort_keys=True, + ) def generate_data_catalogs(start_idx, data_catalog_max_rows, validate_json, type): - print('generating %s data catalogs...' % type) + print("generating %s data catalogs..." % type) test_data_catalog_list = [] - json_schema = get_json_schema('datacatalog') + json_schema = get_json_schema("datacatalog") - with open('data_catalog_test_data_template.json') as json_file: + with open("data_catalog_test_data_template.json") as json_file: row_template = json_load(json_file) for i in range(start_idx, start_idx + data_catalog_max_rows): new = { - 'fields': deepcopy(row_template), - 'model': "metax_api.datacatalog", - 'pk': i, + "fields": deepcopy(row_template), + "model": "metax_api.datacatalog", + "pk": i, } - new['fields']['date_modified'] = '2017-06-15T10:07:22Z' - new['fields']['date_created'] = '2017-05-15T10:07:22Z' - new['fields']['catalog_json']['identifier'] = generate_test_identifier(dc_type, i) + new["fields"]["date_modified"] = "2017-06-15T10:07:22Z" + new["fields"]["date_created"] = "2017-05-15T10:07:22Z" + new["fields"]["catalog_json"]["identifier"] = generate_test_identifier(dc_type, i) - if type == 'ida': - new['fields']['catalog_json']['research_dataset_schema'] = 'ida' - elif type == 'att': - new['fields']['catalog_json']['research_dataset_schema'] = 'att' + if type == "ida": + new["fields"]["catalog_json"]["research_dataset_schema"] = "ida" + elif type == "att": + new["fields"]["catalog_json"]["research_dataset_schema"] = "att" if i in (start_idx, start_idx + 1): # lets pretend that the first two data catalogs will support versioning, # they are "fairdata catalogs" dataset_versioning = True - new['fields']['catalog_json']['harvested'] = False + new["fields"]["catalog_json"]["harvested"] = False else: dataset_versioning = False # rest of the catalogs are harvested - new['fields']['catalog_json']['harvested'] = True + new["fields"]["catalog_json"]["harvested"] = True - new['fields']['catalog_json']['dataset_versioning'] = dataset_versioning + new["fields"]["catalog_json"]["dataset_versioning"] = dataset_versioning test_data_catalog_list.append(new) if validate_json or i == start_idx: - json_validate(new['fields']['catalog_json'], json_schema) + json_validate(new["fields"]["catalog_json"], json_schema) return test_data_catalog_list def generate_contracts(contract_max_rows, validate_json): - print('generating contracts...') + print("generating contracts...") test_contract_list = [] - json_schema = get_json_schema('contract') + json_schema = get_json_schema("contract") - with open('contract_test_data_template.json') as json_file: + with open("contract_test_data_template.json") as json_file: row_template = json_load(json_file) # sample contract provided by PAS new = { - 'fields': deepcopy(row_template[0]), - 'model': "metax_api.contract", - 'pk': 1, + "fields": deepcopy(row_template[0]), + "model": "metax_api.contract", + "pk": 1, } - json_validate(new['fields']['contract_json'], json_schema) + json_validate(new["fields"]["contract_json"], json_schema) test_contract_list.append(new) for i in range(2, contract_max_rows + 1): new = { - 'fields': deepcopy(row_template[1]), - 'model': "metax_api.contract", - 'pk': i, + "fields": deepcopy(row_template[1]), + "model": "metax_api.contract", + "pk": i, } - new['fields']['contract_json']['identifier'] = "optional:contract:identifier%d" % i - new['fields']['contract_json']['title'] = "Title of Contract %d" % i - new['fields']['contract_json']['organization']['organization_identifier'] = "1234567-%d" % i - new['fields']['date_modified'] = '2017-06-15T10:07:22Z' - new['fields']['date_created'] = '2017-05-15T10:07:22Z' + new["fields"]["contract_json"]["identifier"] = "optional:contract:identifier%d" % i + new["fields"]["contract_json"]["title"] = "Title of Contract %d" % i + new["fields"]["contract_json"]["organization"]["organization_identifier"] = "1234567-%d" % i + new["fields"]["date_modified"] = "2017-06-15T10:07:22Z" + new["fields"]["date_created"] = "2017-05-15T10:07:22Z" test_contract_list.append(new) if validate_json or i == 1: - json_validate(new['fields']['contract_json'], json_schema) + json_validate(new["fields"]["contract_json"], json_schema) return test_contract_list -def generate_catalog_records(basic_catalog_record_max_rows, data_catalogs_list, contract_list, file_list, - validate_json, type, test_data_list=[], dataset_version_sets=[]): - print('generating %s catalog records...' % type) +def generate_catalog_records( + basic_catalog_record_max_rows, + data_catalogs_list, + contract_list, + file_list, + validate_json, + type, + test_data_list=[], + dataset_version_sets=[], +): + print("generating %s catalog records..." % type) - with open('catalog_record_test_data_template.json') as json_file: + with open("catalog_record_test_data_template.json") as json_file: row_template = json_load(json_file) files_start_idx = 1 - data_catalog_id = data_catalogs_list[0]['pk'] + data_catalog_id = data_catalogs_list[0]["pk"] owner_idx = 0 start_idx = len(test_data_list) + 1 for i in range(start_idx, start_idx + basic_catalog_record_max_rows): json_schema = None - if type == 'ida': - json_schema = get_json_schema('ida_dataset') - elif type == 'att': - json_schema = get_json_schema('att_dataset') + if type == "ida": + json_schema = get_json_schema("ida_dataset") + elif type == "att": + json_schema = get_json_schema("att_dataset") new = { - 'fields': row_template.copy(), - 'model': 'metax_api.catalogrecord', - 'pk': i, + "fields": row_template.copy(), + "model": "metax_api.catalogrecord", + "pk": i, } - if data_catalog_id in (1, 2): # versioned catalogs only + if data_catalog_id in (1, 2): # versioned catalogs only dataset_version_set = { - 'fields': {}, - 'model': 'metax_api.datasetversionset', - 'pk': i, + "fields": {}, + "model": "metax_api.datasetversionset", + "pk": i, } - new['fields']['dataset_version_set'] = dataset_version_set['pk'] + new["fields"]["dataset_version_set"] = dataset_version_set["pk"] dataset_version_sets.append(dataset_version_set) # comment this line. i dare you. # for real tho, required to prevent some strange behaving references to old data - new['fields']['research_dataset'] = row_template['research_dataset'].copy() - new['fields']['data_catalog'] = data_catalog_id - new['fields']['research_dataset']['metadata_version_identifier'] = generate_test_identifier(cr_type, i, - urn=False) - new['fields']['research_dataset']['preferred_identifier'] = generate_test_identifier(cr_type, i) - new['fields']['identifier'] = generate_test_identifier('cr', i, urn=False) - new['fields']['date_modified'] = '2017-06-23T10:07:22Z' - new['fields']['date_created'] = '2017-05-23T10:07:22Z' - new['fields']['files'] = [] + new["fields"]["research_dataset"] = row_template["research_dataset"].copy() + new["fields"]["data_catalog"] = data_catalog_id + new["fields"]["research_dataset"]["metadata_version_identifier"] = generate_test_identifier( + cr_type, i, urn=False + ) + new["fields"]["research_dataset"]["preferred_identifier"] = generate_test_identifier( + cr_type, i + ) + new["fields"]["identifier"] = generate_test_identifier("cr", i, urn=False) + new["fields"]["date_modified"] = "2017-06-23T10:07:22Z" + new["fields"]["date_created"] = "2017-05-23T10:07:22Z" + new["fields"]["files"] = [] # add files - if type == 'ida': - new['fields']['files'] = [] + if type == "ida": + new["fields"]["files"] = [] dataset_files = [] total_files_byte_size = 0 file_divider = 4 for j in range(files_start_idx, files_start_idx + files_per_dataset): - total_files_byte_size += file_list[j - 1]['fields']['byte_size'] + total_files_byte_size += file_list[j - 1]["fields"]["byte_size"] # note - this field will go in the m2m table in the db when importing generated testdata... - new['fields']['files'].append(file_list[j - 1]['pk']) + new["fields"]["files"].append(file_list[j - 1]["pk"]) # ... while every API operation will look at research_dataset.files.identifier # to lookup the file - be careful the identifier below matches with the m2m id set above - dataset_files.append({ - 'identifier': file_list[j - 1]['fields']['identifier'], - 'title': 'File metadata title %d' % j - }) + dataset_files.append( + { + "identifier": file_list[j - 1]["fields"]["identifier"], + "title": "File metadata title %d" % j, + } + ) if j < file_divider: # first fifth of files - dataset_files[-1]['file_type'] = { + dataset_files[-1]["file_type"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text", + "pref_label": { + "fi": "Teksti", + "en": "Text", + "und": "Teksti" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" } - dataset_files[-1]['use_category'] = { - 'identifier': 'http://uri.suomi.fi/codelist/fairdata/use_category/code/source' + dataset_files[-1]["use_category"] = { + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source", + "pref_label": { + "fi": "Lähdeaineisto", + "en": "Source material", + "und": "Lähdeaineisto" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" } elif file_divider <= j < (file_divider * 2): # second fifth of files - dataset_files[-1]['file_type'] = { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video" + dataset_files[-1]["file_type"] = { + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video", + "pref_label": { + "fi": "Video", + "en": "Video", + "und": "Video" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" } - dataset_files[-1]['use_category'] = { - 'identifier': 'http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome' + dataset_files[-1]["use_category"] = { + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome", + "pref_label": { + "fi": "Tulosaineisto", + "en": "Outcome material", + "und": "Tulosaineisto" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" } elif (file_divider * 2) <= j < (file_divider * 3): # third fifth of files - dataset_files[-1]['file_type'] = { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image" + dataset_files[-1]["file_type"] = { + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image", + "pref_label": { + "fi": "Kuva", + "en": "Image", + "und": "Kuva" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" } - dataset_files[-1]['use_category'] = { - 'identifier': 'http://uri.suomi.fi/codelist/fairdata/use_category/code/publication' + dataset_files[-1]["use_category"] = { + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication", + "pref_label": { + "fi": "Julkaisu", + "en": "Publication", + "und": "Julkaisu" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" } elif (file_divider * 3) <= j < (file_divider * 4): # fourth fifth of files - dataset_files[-1]['file_type'] = { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code" + dataset_files[-1]["file_type"] = { + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code", + "pref_label": { + "fi": "Lähdekoodi", + "en": "Source code", + "und": "Lähdekoodi" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" } - dataset_files[-1]['use_category'] = { - 'identifier': 'http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation' + dataset_files[-1]["use_category"] = { + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation", + "pref_label": { + "fi": "Dokumentaatio", + "en": "Documentation", + "und": "Dokumentaatio" + }, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" } else: # the rest of files - dataset_files[-1]['use_category'] = { - 'identifier': 'configuration' - } + dataset_files[-1]["use_category"] = {"identifier": "configuration"} - new['fields']['research_dataset']['files'] = dataset_files - new['fields']['research_dataset']['total_files_byte_size'] = total_files_byte_size + new["fields"]["research_dataset"]["files"] = dataset_files + new["fields"]["research_dataset"]["total_files_byte_size"] = total_files_byte_size files_start_idx += files_per_dataset - elif type == 'att': - new['fields']['research_dataset']['remote_resources'] = [ + elif type == "att": + new["fields"]["research_dataset"]["remote_resources"] = [ { "title": "Remote resource {0}".format(str(i)), "modified": "2014-01-12T17:11:54Z", "use_category": {"identifier": "outcome"}, - "checksum": {"algorithm": "SHA-256", "checksum_value": "u5y6f4y68765ngf6ry8n"}, - "byte_size": i * 512 + "checksum": { + "algorithm": "SHA-256", + "checksum_value": "u5y6f4y68765ngf6ry8n", + }, + "byte_size": i * 512, }, { "title": "Other remote resource {0}".format(str(i)), "modified": "2013-01-12T11:11:54Z", "use_category": {"identifier": "source"}, - "checksum": {"algorithm": "SHA-512", "checksum_value": "u3k4kn7n1g56l6rq5a5s"}, - "byte_size": i * 1024 - } + "checksum": { + "algorithm": "SHA-512", + "checksum_value": "u3k4kn7n1g56l6rq5a5s", + }, + "byte_size": i * 1024, + }, ] total_remote_resources_byte_size = 0 - for rr in new['fields']['research_dataset']['remote_resources']: - total_remote_resources_byte_size += rr.get('byte_size', 0) - new['fields']['research_dataset'][ - 'total_remote_resources_byte_size'] = total_remote_resources_byte_size + for rr in new["fields"]["research_dataset"]["remote_resources"]: + total_remote_resources_byte_size += rr.get("byte_size", 0) + new["fields"]["research_dataset"][ + "total_remote_resources_byte_size" + ] = total_remote_resources_byte_size if validate_json or i == start_idx: - json_validate(new['fields']['research_dataset'], json_schema) + json_validate(new["fields"]["research_dataset"], json_schema) test_data_list.append(new) @@ -465,99 +578,99 @@ def generate_catalog_records(basic_catalog_record_max_rows, data_catalogs_list, # see CatalogRecord model PRESERVATION_STATE_... for value definitions for i, pres_state_value in enumerate((0, 10, 10, 40, 50, 70, 70, 90, 130, 140)): - test_data_list[i]['fields']['preservation_state'] = pres_state_value + test_data_list[i]["fields"]["preservation_state"] = pres_state_value if i > 0: - test_data_list[i]['fields']['contract'] = 1 + test_data_list[i]["fields"]["contract"] = 1 if i in (90, 140): # packaging, dissemination - test_data_list[i]['fields']['mets_object_identifier'] = ["a", "b", "c"] - - test_data_list[i]['fields']['research_dataset']['curator'] = [{ - "@type": "Person", - "name": "Rahikainen", - "identifier": "id:of:curator:rahikainen", - "member_of": { - "@type": "Organization", - "name": { - "fi": "MysteeriOrganisaatio" - } + test_data_list[i]["fields"]["mets_object_identifier"] = ["a", "b", "c"] + + test_data_list[i]["fields"]["research_dataset"]["curator"] = [ + { + "@type": "Person", + "name": "Rahikainen", + "identifier": "id:of:curator:rahikainen", + "member_of": { + "@type": "Organization", + "name": {"fi": "MysteeriOrganisaatio"}, + }, } - }] + ] # set different owner for i in range(start_idx + 5, len(test_data_list)): - test_data_list[i]['fields']['research_dataset']['curator'] = [{ - "@type": "Person", - "name": "Jarski", - "identifier": "id:of:curator:jarski", - "member_of": { - "@type": "Organization", - "name": { - "fi": "MysteeriOrganisaatio" - } + test_data_list[i]["fields"]["research_dataset"]["curator"] = [ + { + "@type": "Person", + "name": "Jarski", + "identifier": "id:of:curator:jarski", + "member_of": { + "@type": "Organization", + "name": {"fi": "MysteeriOrganisaatio"}, + }, } - }] + ] # if preservation_state is other than 0, means it has been modified at some point, # so set timestamp for i in range(start_idx - 1, len(test_data_list)): row = test_data_list[i] - if row['fields']['preservation_state'] != 0: - row['fields']['preservation_state_modified'] = '2017-05-23T10:07:22.559656Z' + if row["fields"]["preservation_state"] != 0: + row["fields"]["preservation_state_modified"] = "2017-05-23T10:07:22.559656Z" # add a couple of catalog records with fuller research_dataset fields belonging to both ida and att data catalog total_files_byte_size = 0 - if type == 'ida': - template = 'catalog_record_test_data_template_full_ida.json' - elif type == 'att': - template = 'catalog_record_test_data_template_full_att.json' + if type == "ida": + template = "catalog_record_test_data_template_full_ida.json" + elif type == "att": + template = "catalog_record_test_data_template_full_att.json" with open(template) as json_file: row_template_full = json_load(json_file) for j in [0, 1, 2]: new = { - 'fields': deepcopy(row_template_full), - 'model': 'metax_api.catalogrecord', - 'pk': len(test_data_list) + 1, + "fields": deepcopy(row_template_full), + "model": "metax_api.catalogrecord", + "pk": len(test_data_list) + 1, } - if data_catalog_id in (1, 2): # versioned catalogs only + if data_catalog_id in (1, 2): # versioned catalogs only dataset_version_set = { - 'fields': {}, - 'model': 'metax_api.datasetversionset', - 'pk': len(test_data_list) + 1, + "fields": {}, + "model": "metax_api.datasetversionset", + "pk": len(test_data_list) + 1, } - new['fields']['dataset_version_set'] = dataset_version_set['pk'] + new["fields"]["dataset_version_set"] = dataset_version_set["pk"] dataset_version_sets.append(dataset_version_set) # for the relation in the db. includes dir id 3, which includes all 20 files - new['fields']['data_catalog'] = data_catalog_id - new['fields']['date_modified'] = '2017-09-23T10:07:22Z' - new['fields']['date_created'] = '2017-05-23T10:07:22Z' - new['fields']['editor'] = { - 'owner_id': catalog_records_owner_ids[j], - 'creator_id': catalog_records_owner_ids[owner_idx], - } - - new['fields']['research_dataset']['metadata_version_identifier'] = \ - generate_test_identifier(cr_type, len(test_data_list) + 1, urn=False) - new['fields']['research_dataset']['preferred_identifier'] = \ - generate_test_identifier(cr_type, len(test_data_list) + 1) - new['fields']['identifier'] = generate_test_identifier('cr', len(test_data_list) + 1, urn=False) - - if type == 'ida': + new["fields"]["data_catalog"] = data_catalog_id + new["fields"]["date_modified"] = "2017-09-23T10:07:22Z" + new["fields"]["date_created"] = "2017-05-23T10:07:22Z" + + new["fields"]["research_dataset"]["metadata_version_identifier"] = generate_test_identifier( + cr_type, len(test_data_list) + 1, urn=False + ) + new["fields"]["research_dataset"]["preferred_identifier"] = generate_test_identifier( + cr_type, len(test_data_list) + 1 + ) + new["fields"]["identifier"] = generate_test_identifier( + "cr", len(test_data_list) + 1, urn=False + ) + + if type == "ida": if j in [0, 1]: - new['fields']['files'] = [i for i in range(1, 21)] - file_identifier_0 = file_list[0]['fields']['identifier'] - file_identifier_1 = file_list[1]['fields']['identifier'] - total_files_byte_size = sum(f['fields']['byte_size'] for f in file_list[0:19]) - new['fields']['research_dataset']['total_files_byte_size'] = total_files_byte_size - new['fields']['research_dataset']['files'][0]['identifier'] = file_identifier_0 - new['fields']['research_dataset']['files'][1]['identifier'] = file_identifier_1 + new["fields"]["files"] = [i for i in range(1, 21)] + file_identifier_0 = file_list[0]["fields"]["identifier"] + file_identifier_1 = file_list[1]["fields"]["identifier"] + total_files_byte_size = sum(f["fields"]["byte_size"] for f in file_list[0:19]) + new["fields"]["research_dataset"]["total_files_byte_size"] = total_files_byte_size + new["fields"]["research_dataset"]["files"][0]["identifier"] = file_identifier_0 + new["fields"]["research_dataset"]["files"][1]["identifier"] = file_identifier_1 elif j == 2: db_files = [] directories = [] @@ -576,11 +689,9 @@ def generate_catalog_records(basic_catalog_record_max_rows, data_catalogs_list, "definition": { "en": "A statement or formal explanation of the meaning of a concept." }, - "in_scheme": "http://uri.of.filetype.concept/scheme" + "in_scheme": "http://uri.of.filetype.concept/scheme", }, - "use_category": { - "identifier": "configuration" - } + "use_category": {"identifier": "configuration"}, }, { "identifier": "pid:urn:28", @@ -591,12 +702,10 @@ def generate_catalog_records(basic_catalog_record_max_rows, data_catalogs_list, "definition": { "en": "A statement or formal explanation of the meaning of a concept." }, - "in_scheme": "http://uri.of.filetype.concept/scheme" + "in_scheme": "http://uri.of.filetype.concept/scheme", }, - "use_category": { - "identifier": "publication" - } - } + "use_category": {"identifier": "publication"}, + }, ] directories = [ @@ -604,58 +713,52 @@ def generate_catalog_records(basic_catalog_record_max_rows, data_catalogs_list, "identifier": "pid:urn:dir:18", "title": "Phase 1 of science data C", "description": "Description of the directory", - "use_category": { - "identifier": "outcome" - } + "use_category": {"identifier": "outcome"}, }, { "identifier": "pid:urn:dir:22", "title": "Phase 2 of science data C", "description": "Description of the directory", - "use_category": { - "identifier": "outcome" - } + "use_category": {"identifier": "outcome"}, }, { "identifier": "pid:urn:dir:12", "title": "Phase 1 01/2018 of Science data A", "description": "Description of the directory", - "use_category": { - "identifier": "outcome" - } + "use_category": {"identifier": "outcome"}, }, { "identifier": "pid:urn:dir:13", "title": "Science data B", "description": "Description of the directory", - "use_category": { - "identifier": "source" - } + "use_category": {"identifier": "source"}, }, { "identifier": "pid:urn:dir:14", "title": "Other stuff", "description": "Description of the directory", - "use_category": { - "identifier": "method" - } - } + "use_category": {"identifier": "method"}, + }, ] - total_files_byte_size += sum(file_list[file_pk - 1]['fields']['byte_size'] for file_pk in db_files) + total_files_byte_size += sum( + file_list[file_pk - 1]["fields"]["byte_size"] for file_pk in db_files + ) - new['fields']['files'] = db_files - new['fields']['research_dataset']['files'] = files - new['fields']['research_dataset']['directories'] = directories - new['fields']['research_dataset']['total_files_byte_size'] = total_files_byte_size - elif type == 'att': + new["fields"]["files"] = db_files + new["fields"]["research_dataset"]["files"] = files + new["fields"]["research_dataset"]["directories"] = directories + new["fields"]["research_dataset"]["total_files_byte_size"] = total_files_byte_size + elif type == "att": total_remote_resources_byte_size = 0 - if 'remote_resources' in new['fields']['research_dataset']: - for rr in new['fields']['research_dataset']['remote_resources']: - total_remote_resources_byte_size += rr.get('byte_size', 0) - new['fields']['research_dataset']['total_remote_resources_byte_size'] = total_remote_resources_byte_size - - json_validate(new['fields']['research_dataset'], json_schema) + if "remote_resources" in new["fields"]["research_dataset"]: + for rr in new["fields"]["research_dataset"]["remote_resources"]: + total_remote_resources_byte_size += rr.get("byte_size", 0) + new["fields"]["research_dataset"][ + "total_remote_resources_byte_size" + ] = total_remote_resources_byte_size + + json_validate(new["fields"]["research_dataset"], json_schema) test_data_list.append(new) return test_data_list, dataset_version_sets @@ -665,41 +768,44 @@ def generate_alt_catalog_records(test_data_list): # # create a couple of alternate records for record with id 10 # - # note, these alt records wont have an editor-field set, since they presumably - # originated to metax from somewhere else than qvain (were harvested). - # - print('generating alternate catalog records...') + print("generating alternate catalog records...") alternate_record_set = { - 'fields': {}, - 'model': 'metax_api.alternaterecordset', - 'pk': 1, + "fields": {}, + "model": "metax_api.alternaterecordset", + "pk": 1, } # first record belongs to alt record set - test_data_list[9]['fields']['alternate_record_set'] = 1 + test_data_list[9]["fields"]["alternate_record_set"] = 1 # create one other record alt_rec = deepcopy(test_data_list[9]) - alt_rec['pk'] = test_data_list[-1]['pk'] + 1 - alt_rec['fields']['research_dataset']['preferred_identifier'] = test_data_list[9]['fields']['research_dataset'][ - 'preferred_identifier'] - alt_rec['fields']['research_dataset']['metadata_version_identifier'] += '-alt-1' - alt_rec['fields']['identifier'] = generate_test_identifier('cr', len(test_data_list) + 1, urn=False) - alt_rec['fields']['data_catalog'] = 2 - alt_rec['fields']['alternate_record_set'] = 1 - alt_rec['fields'].pop('dataset_version_set', None) + alt_rec["pk"] = test_data_list[-1]["pk"] + 1 + alt_rec["fields"]["research_dataset"]["preferred_identifier"] = test_data_list[9]["fields"][ + "research_dataset" + ]["preferred_identifier"] + alt_rec["fields"]["research_dataset"]["metadata_version_identifier"] += "-alt-1" + alt_rec["fields"]["identifier"] = generate_test_identifier( + "cr", len(test_data_list) + 1, urn=False + ) + alt_rec["fields"]["data_catalog"] = 2 + alt_rec["fields"]["alternate_record_set"] = 1 + alt_rec["fields"].pop("dataset_version_set", None) test_data_list.append(alt_rec) # create second other record alt_rec = deepcopy(test_data_list[9]) - alt_rec['pk'] = test_data_list[-1]['pk'] + 1 - alt_rec['fields']['research_dataset']['preferred_identifier'] = test_data_list[9]['fields']['research_dataset'][ - 'preferred_identifier'] - alt_rec['fields']['research_dataset']['metadata_version_identifier'] += '-alt-2' - alt_rec['fields']['identifier'] = generate_test_identifier('cr', len(test_data_list) + 1, urn=False) - alt_rec['fields']['data_catalog'] = 3 - alt_rec['fields']['alternate_record_set'] = 1 - alt_rec['fields'].pop('dataset_version_set', None) + alt_rec["pk"] = test_data_list[-1]["pk"] + 1 + alt_rec["fields"]["research_dataset"]["preferred_identifier"] = test_data_list[9]["fields"][ + "research_dataset" + ]["preferred_identifier"] + alt_rec["fields"]["research_dataset"]["metadata_version_identifier"] += "-alt-2" + alt_rec["fields"]["identifier"] = generate_test_identifier( + "cr", len(test_data_list) + 1, urn=False + ) + alt_rec["fields"]["data_catalog"] = 3 + alt_rec["fields"]["alternate_record_set"] = 1 + alt_rec["fields"].pop("dataset_version_set", None) test_data_list.append(alt_rec) # alternate record set must exist before importing catalog records, so prepend it @@ -708,50 +814,68 @@ def generate_alt_catalog_records(test_data_list): def set_qvain_info_to_records(catalog_record_list): - ''' + """ For data catalog ids 1,2 set qvain info, since they are supposedly fairdata catalogs. - ''' + """ owner_idx = 0 total_qvain_users = len(catalog_records_owner_ids) for cr in catalog_record_list: - if cr['model'] != 'metax_api.catalogrecord': + if cr["model"] != "metax_api.catalogrecord": # there are other type of objects in the list (at least datasetversionset) continue - if cr['fields']['data_catalog'] not in (1, 2): + if cr["fields"]["data_catalog"] not in (1, 2): continue - cr['fields']['editor'] = { - 'owner_id': catalog_records_owner_ids[owner_idx], - 'creator_id': catalog_records_owner_ids[owner_idx], - 'identifier': 'qvain', - 'record_id': '955e904-e3dd-4d7e-99f1-3fed446f9%03d' % cr['pk'], # 3 leading zeroes to preserve length - } owner_idx += 1 if owner_idx >= total_qvain_users: owner_idx = 0 -if __name__ == '__main__': - print('begin generating test data...') +if __name__ == "__main__": + print("begin generating test data...") contract_list = generate_contracts(contract_max_rows, validate_json) file_storage_list = generate_file_storages(file_storage_max_rows) file_list, directory_list = generate_files(file_storage_list, validate_json) - ida_data_catalogs_list = generate_data_catalogs(1, ida_data_catalog_max_rows, validate_json, 'ida') - att_data_catalogs_list = generate_data_catalogs(ida_data_catalog_max_rows + 1, att_data_catalog_max_rows, - validate_json, 'att') - - catalog_record_list, dataset_version_sets = generate_catalog_records(ida_catalog_record_max_rows, - ida_data_catalogs_list, contract_list, file_list, validate_json, 'ida') - - catalog_record_list, dataset_version_sets = generate_catalog_records(att_catalog_record_max_rows, - att_data_catalogs_list, contract_list, [], validate_json, 'att', catalog_record_list, dataset_version_sets) + ida_data_catalogs_list = generate_data_catalogs( + 1, ida_data_catalog_max_rows, validate_json, "ida" + ) + att_data_catalogs_list = generate_data_catalogs( + ida_data_catalog_max_rows + 1, att_data_catalog_max_rows, validate_json, "att" + ) + + catalog_record_list, dataset_version_sets = generate_catalog_records( + ida_catalog_record_max_rows, + ida_data_catalogs_list, + contract_list, + file_list, + validate_json, + "ida", + ) + + catalog_record_list, dataset_version_sets = generate_catalog_records( + att_catalog_record_max_rows, + att_data_catalogs_list, + contract_list, + [], + validate_json, + "att", + catalog_record_list, + dataset_version_sets, + ) catalog_record_list = generate_alt_catalog_records(catalog_record_list) set_qvain_info_to_records(catalog_record_list) - save_test_data(file_storage_list, directory_list, file_list, ida_data_catalogs_list + att_data_catalogs_list, - contract_list, catalog_record_list, dataset_version_sets) - - print('done') + save_test_data( + file_storage_list, + directory_list, + file_list, + ida_data_catalogs_list + att_data_catalogs_list, + contract_list, + catalog_record_list, + dataset_version_sets, + ) + + print("done") diff --git a/src/metax_api/tests/testdata/request_test.py b/src/metax_api/tests/testdata/request_test.py index 56085e2b..20a06868 100755 --- a/src/metax_api/tests/testdata/request_test.py +++ b/src/metax_api/tests/testdata/request_test.py @@ -12,7 +12,7 @@ d = print d("STARTING MEASUREMENTS") -url = 'https://metax.csc.local/rest/files/urn:nbn:fi:csc-ida201401200000100000' +url = "https://metax.csc.local/rest/files/urn:nbn:fi:csc-ida201401200000100000" # url = 'https://metax.csc.local/rest/files/d1a53535-2adb-5184-a10b-8dd7338d0a41' times = [] run_times = 20 diff --git a/src/metax_api/tests/testdata/test_data.json b/src/metax_api/tests/testdata/test_data.json index c9bac66e..c3802a10 100755 --- a/src/metax_api/tests/testdata/test_data.json +++ b/src/metax_api/tests/testdata/test_data.json @@ -4426,7 +4426,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { "en": "Open", - "fi": "Avoin" + "fi": "Avoin", + "und": "Avoin" } } ], @@ -4457,8 +4458,9 @@ { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0", "title": { - "en": "CC BY 4.0", - "fi": "CC BY 4.0" + "en": "Creative Commons Attribution 4.0 International (CC BY 4.0)", + "fi": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)", + "und": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)" } } ] @@ -4494,10 +4496,22 @@ "issued": "2014-02-27", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/fin" + "identifier": "http://lexvo.org/id/iso639-3/fin", + "title": { + "en": "Finnish", + "fi": "suomi", + "sv": "finska", + "und": "suomi" + } }, { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "modified": "2014-01-17T08:19:58Z", @@ -4542,7 +4556,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { "en": "Open", - "fi": "Avoin" + "fi": "Avoin", + "und": "Avoin" } } ], @@ -4573,8 +4588,9 @@ { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0", "title": { - "en": "CC BY 4.0", - "fi": "CC BY 4.0" + "en": "Creative Commons Attribution 4.0 International (CC BY 4.0)", + "fi": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)", + "und": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)" } } ] @@ -4610,10 +4626,22 @@ "issued": "2014-02-27", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/fin" + "identifier": "http://lexvo.org/id/iso639-3/fin", + "title": { + "en": "Finnish", + "fi": "suomi", + "sv": "finska", + "und": "suomi" + } }, { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "modified": "2014-01-17T08:19:58Z", @@ -4658,7 +4686,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { "en": "Open", - "fi": "Avoin" + "fi": "Avoin", + "und": "Avoin" } } ], @@ -4689,8 +4718,9 @@ { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0", "title": { - "en": "CC BY 4.0", - "fi": "CC BY 4.0" + "en": "Creative Commons Attribution 4.0 International (CC BY 4.0)", + "fi": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)", + "und": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)" } } ] @@ -4726,10 +4756,22 @@ "issued": "2014-02-27", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/fin" + "identifier": "http://lexvo.org/id/iso639-3/fin", + "title": { + "en": "Finnish", + "fi": "suomi", + "sv": "finska", + "und": "suomi" + } }, { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "modified": "2014-01-17T08:19:58Z", @@ -4774,7 +4816,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { "en": "Open", - "fi": "Avoin" + "fi": "Avoin", + "und": "Avoin" } } ], @@ -4805,8 +4848,9 @@ { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0", "title": { - "en": "CC BY 4.0", - "fi": "CC BY 4.0" + "en": "Creative Commons Attribution 4.0 International (CC BY 4.0)", + "fi": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)", + "und": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)" } } ] @@ -4842,10 +4886,22 @@ "issued": "2014-02-27", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/fin" + "identifier": "http://lexvo.org/id/iso639-3/fin", + "title": { + "en": "Finnish", + "fi": "suomi", + "sv": "finska", + "und": "suomi" + } }, { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "modified": "2014-01-17T08:19:58Z", @@ -4890,7 +4946,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { "en": "Open", - "fi": "Avoin" + "fi": "Avoin", + "und": "Avoin" } } ], @@ -4921,8 +4978,9 @@ { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0", "title": { - "en": "CC BY 4.0", - "fi": "CC BY 4.0" + "en": "Creative Commons Attribution 4.0 International (CC BY 4.0)", + "fi": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)", + "und": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)" } } ] @@ -4958,10 +5016,22 @@ "issued": "2014-02-27", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/fin" + "identifier": "http://lexvo.org/id/iso639-3/fin", + "title": { + "en": "Finnish", + "fi": "suomi", + "sv": "finska", + "und": "suomi" + } }, { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "modified": "2014-01-17T08:19:58Z", @@ -5006,7 +5076,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { "en": "Open", - "fi": "Avoin" + "fi": "Avoin", + "und": "Avoin" } } ], @@ -5037,8 +5108,9 @@ { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0", "title": { - "en": "CC BY 4.0", - "fi": "CC BY 4.0" + "en": "Creative Commons Attribution 4.0 International (CC BY 4.0)", + "fi": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)", + "und": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)" } } ] @@ -5074,10 +5146,22 @@ "issued": "2014-02-27", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/fin" + "identifier": "http://lexvo.org/id/iso639-3/fin", + "title": { + "en": "Finnish", + "fi": "suomi", + "sv": "finska", + "und": "suomi" + } }, { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "modified": "2014-01-17T08:19:58Z", @@ -5122,7 +5206,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { "en": "Open", - "fi": "Avoin" + "fi": "Avoin", + "und": "Avoin" } } ], @@ -5153,8 +5238,9 @@ { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0", "title": { - "en": "CC BY 4.0", - "fi": "CC BY 4.0" + "en": "Creative Commons Attribution 4.0 International (CC BY 4.0)", + "fi": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)", + "und": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)" } } ] @@ -5190,10 +5276,22 @@ "issued": "2014-02-27", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/fin" + "identifier": "http://lexvo.org/id/iso639-3/fin", + "title": { + "en": "Finnish", + "fi": "suomi", + "sv": "finska", + "und": "suomi" + } }, { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "modified": "2014-01-17T08:19:58Z", @@ -5238,7 +5336,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", "pref_label": { "en": "Open", - "fi": "Avoin" + "fi": "Avoin", + "und": "Avoin" } } ], @@ -5269,8 +5368,9 @@ { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0", "title": { - "en": "CC BY 4.0", - "fi": "CC BY 4.0" + "en": "Creative Commons Attribution 4.0 International (CC BY 4.0)", + "fi": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)", + "und": "Creative Commons Nime\u00e4 4.0 Kansainv\u00e4linen (CC BY 4.0)" } } ] @@ -5306,10 +5406,22 @@ "issued": "2014-02-27", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/fin" + "identifier": "http://lexvo.org/id/iso639-3/fin", + "title": { + "en": "Finnish", + "fi": "suomi", + "sv": "finska", + "und": "suomi" + } }, { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "modified": "2014-01-17T08:19:58Z", @@ -5615,12 +5727,6 @@ "dataset_version_set": 1, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "053bffbcc41edad4853bea91fc42ea18", - "identifier": "qvain", - "owner_id": "053bffbcc41edad4853bea91fc42ea18", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9001" - }, "files": [ 1, 2 @@ -5635,7 +5741,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -5643,7 +5755,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -5674,29 +5793,59 @@ "files": [ { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Text", + "fi": "Teksti", + "und": "Teksti" + } }, "identifier": "pid:urn:1", "title": "File metadata title 1", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Source material", + "fi": "L\u00e4hdeaineisto", + "und": "L\u00e4hdeaineisto" + } } }, { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Text", + "fi": "Teksti", + "und": "Teksti" + } }, "identifier": "pid:urn:2", "title": "File metadata title 2", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Source material", + "fi": "L\u00e4hdeaineisto", + "und": "L\u00e4hdeaineisto" + } } } ], "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f96d1", @@ -5733,12 +5882,6 @@ "dataset_version_set": 2, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "053d18ecb29e752cb7a35cd77b34f5fd", - "identifier": "qvain", - "owner_id": "053d18ecb29e752cb7a35cd77b34f5fd", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9002" - }, "files": [ 3, 4 @@ -5753,7 +5896,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -5761,7 +5910,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -5792,29 +5948,59 @@ "files": [ { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Text", + "fi": "Teksti", + "und": "Teksti" + } }, "identifier": "pid:urn:3", "title": "File metadata title 3", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Source material", + "fi": "L\u00e4hdeaineisto", + "und": "L\u00e4hdeaineisto" + } } }, { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Video", + "fi": "Video", + "und": "Video" + } }, "identifier": "pid:urn:4", "title": "File metadata title 4", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Outcome material", + "fi": "Tulosaineisto", + "und": "Tulosaineisto" + } } } ], "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f96d2", @@ -5851,12 +6037,6 @@ "dataset_version_set": 3, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "05593961536b76fa825281ccaedd4d4f", - "identifier": "qvain", - "owner_id": "05593961536b76fa825281ccaedd4d4f", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9003" - }, "files": [ 5, 6 @@ -5871,7 +6051,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -5879,7 +6065,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -5910,29 +6103,59 @@ "files": [ { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Video", + "fi": "Video", + "und": "Video" + } }, "identifier": "pid:urn:5", "title": "File metadata title 5", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Outcome material", + "fi": "Tulosaineisto", + "und": "Tulosaineisto" + } } }, { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Video", + "fi": "Video", + "und": "Video" + } }, "identifier": "pid:urn:6", "title": "File metadata title 6", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Outcome material", + "fi": "Tulosaineisto", + "und": "Tulosaineisto" + } } } ], "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f96d3", @@ -5969,12 +6192,6 @@ "dataset_version_set": 4, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "055ea4dade5ab2145954f56d4b51cef0", - "identifier": "qvain", - "owner_id": "055ea4dade5ab2145954f56d4b51cef0", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9004" - }, "files": [ 7, 8 @@ -5989,7 +6206,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -5997,7 +6220,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -6028,29 +6258,59 @@ "files": [ { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Video", + "fi": "Video", + "und": "Video" + } }, "identifier": "pid:urn:7", "title": "File metadata title 7", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Outcome material", + "fi": "Tulosaineisto", + "und": "Tulosaineisto" + } } }, { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Image", + "fi": "Kuva", + "und": "Kuva" + } }, "identifier": "pid:urn:8", "title": "File metadata title 8", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Publication", + "fi": "Julkaisu", + "und": "Julkaisu" + } } } ], "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f96d4", @@ -6087,12 +6347,6 @@ "dataset_version_set": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "055ea531a6cac569425bed94459266ee", - "identifier": "qvain", - "owner_id": "055ea531a6cac569425bed94459266ee", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9005" - }, "files": [ 9, 10 @@ -6107,7 +6361,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -6115,7 +6375,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -6146,29 +6413,59 @@ "files": [ { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Image", + "fi": "Kuva", + "und": "Kuva" + } }, "identifier": "pid:urn:9", "title": "File metadata title 9", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Publication", + "fi": "Julkaisu", + "und": "Julkaisu" + } } }, { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Image", + "fi": "Kuva", + "und": "Kuva" + } }, "identifier": "pid:urn:10", "title": "File metadata title 10", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Publication", + "fi": "Julkaisu", + "und": "Julkaisu" + } } } ], "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f96d5", @@ -6205,12 +6502,6 @@ "dataset_version_set": 6, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "053bffbcc41edad4853bea91fc42ea18", - "identifier": "qvain", - "owner_id": "053bffbcc41edad4853bea91fc42ea18", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9006" - }, "files": [ 11, 12 @@ -6225,7 +6516,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -6233,7 +6530,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -6264,29 +6568,59 @@ "files": [ { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Image", + "fi": "Kuva", + "und": "Kuva" + } }, "identifier": "pid:urn:11", "title": "File metadata title 11", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Publication", + "fi": "Julkaisu", + "und": "Julkaisu" + } } }, { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Source code", + "fi": "L\u00e4hdekoodi", + "und": "L\u00e4hdekoodi" + } }, "identifier": "pid:urn:12", "title": "File metadata title 12", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Documentation", + "fi": "Dokumentaatio", + "und": "Dokumentaatio" + } } } ], "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f96d6", @@ -6323,12 +6657,6 @@ "dataset_version_set": 7, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "053d18ecb29e752cb7a35cd77b34f5fd", - "identifier": "qvain", - "owner_id": "053d18ecb29e752cb7a35cd77b34f5fd", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9007" - }, "files": [ 13, 14 @@ -6343,7 +6671,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -6351,7 +6685,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -6382,29 +6723,59 @@ "files": [ { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Source code", + "fi": "L\u00e4hdekoodi", + "und": "L\u00e4hdekoodi" + } }, "identifier": "pid:urn:13", "title": "File metadata title 13", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Documentation", + "fi": "Dokumentaatio", + "und": "Dokumentaatio" + } } }, { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Source code", + "fi": "L\u00e4hdekoodi", + "und": "L\u00e4hdekoodi" + } }, "identifier": "pid:urn:14", "title": "File metadata title 14", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Documentation", + "fi": "Dokumentaatio", + "und": "Dokumentaatio" + } } } ], "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f96d7", @@ -6441,12 +6812,6 @@ "dataset_version_set": 8, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "05593961536b76fa825281ccaedd4d4f", - "identifier": "qvain", - "owner_id": "05593961536b76fa825281ccaedd4d4f", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9008" - }, "files": [ 15, 16 @@ -6461,7 +6826,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -6469,7 +6840,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -6500,12 +6878,24 @@ "files": [ { "file_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code" + "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/source_code", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", + "pref_label": { + "en": "Source code", + "fi": "L\u00e4hdekoodi", + "und": "L\u00e4hdekoodi" + } }, "identifier": "pid:urn:15", "title": "File metadata title 15", "use_category": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation" + "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", + "pref_label": { + "en": "Documentation", + "fi": "Dokumentaatio", + "und": "Dokumentaatio" + } } }, { @@ -6519,7 +6909,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f96d8", @@ -6556,12 +6952,6 @@ "dataset_version_set": 9, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "055ea4dade5ab2145954f56d4b51cef0", - "identifier": "qvain", - "owner_id": "055ea4dade5ab2145954f56d4b51cef0", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9009" - }, "files": [ 17, 18 @@ -6576,7 +6966,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -6584,7 +6980,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -6631,7 +7034,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f96d9", @@ -6669,12 +7078,6 @@ "dataset_version_set": 10, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "055ea531a6cac569425bed94459266ee", - "identifier": "qvain", - "owner_id": "055ea531a6cac569425bed94459266ee", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9010" - }, "files": [ 19, 20 @@ -6689,7 +7092,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -6697,7 +7106,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -6744,7 +7160,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9610", @@ -6779,12 +7201,6 @@ "dataset_version_set": 11, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", - "editor": { - "creator_id": "053bffbcc41edad4853bea91fc42ea18", - "identifier": "qvain", - "owner_id": "053bffbcc41edad4853bea91fc42ea18", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9011" - }, "files": [ 1, 2, @@ -6814,13 +7230,12 @@ "research_dataset": { "access_rights": { "access_type": { - "definition": { - "en": "A statement or formal explanation of the meaning of a concept." - }, "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", - "in_scheme": "http://uri.of.filetype.concept/scheme", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", "pref_label": { - "en": "pref label for this type" + "en": "Open", + "fi": "Avoin", + "und": "Avoin" } }, "access_url": { @@ -6844,7 +7259,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } }, { @@ -6852,20 +7268,32 @@ "en": "Free account of the rights" }, "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-2.0", + "license": "https://creativecommons.org/licenses/by-nc/2.0/", "title": { - "en": "A name given to the resource" + "en": "Creative Commons Attribution-NonCommercial 2.0 Generic (CC BY-NC 2.0", + "fi": "Creative Commons Nime\u00e4-EiKaupallinen 2.0 Yleinen (CC BY-NC 2.0)", + "und": "Creative Commons Nime\u00e4-EiKaupallinen 2.0 Yleinen (CC BY-NC 2.0)" } }, { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/other", "title": { - "en": "A name given to the resource" + "en": "Other", + "fi": "Muu", + "und": "Muu" } } ], "restriction_grounds": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other" + "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds", + "pref_label": { + "en": "Restricted access due to other reasons", + "fi": "Saatavuutta rajoitettu muulla perusteella", + "sv": "Begr\u00e4nsad \u00e5tkomst av \u00f6vriga sk\u00e4l", + "und": "Saatavuutta rajoitettu muulla perusteella" + } } ] }, @@ -6875,7 +7303,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Funding acquisition", + "fi": "Rahoituksen hankinta", + "sv": "Anskaffande av finansiering", + "und": "Rahoituksen hankinta" + } } ], "email": "kalle.kontribuuttaaja@csc.fi", @@ -6901,7 +7336,14 @@ "@type": "Person", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Project Leader", + "fi": "Projektin johtaja", + "sv": "Projektledare", + "und": "Projektin johtaja" + } } ], "email": "franzibald.kontribuuttaaja@csc.fi", @@ -6929,7 +7371,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Conceptualization", + "fi": "Tutkimuksen muotoilu", + "sv": "Formulering av forskningen", + "und": "Tutkimuksen muotoilu" + } } ], "member_of": { @@ -6948,10 +7397,24 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } }, { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Sponsor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Sponsor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Sponsor", + "fi": "Sponsori", + "sv": "Sponsor", + "und": "Sponsori" + } } ], "identifier": "id:of:curator:default", @@ -7036,13 +7499,25 @@ ], "infrastructure": [ { - "identifier": "urn-nbn-fi-research-infras-2016072530" + "identifier": "urn-nbn-fi-research-infras-2016072530", + "in_scheme": "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures", + "pref_label": { + "en": "Accelerator laboratory of the university of Jyv\u00e4skyl\u00e4", + "fi": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio", + "und": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio" + } } ], "is_output_of": [ { "funder_type": { - "identifier": "tekes" + "identifier": "http://uri.suomi.fi/codelist/fairdata/funder_type/code/tekes", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/funder_type", + "pref_label": { + "en": "Tekes", + "fi": "Tekes", + "und": "Tekes" + } }, "has_funder_identifier": "funderprojectidentifier", "has_funding_agency": [ @@ -7050,7 +7525,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Editor", + "fi": "Julkaisutoimittaja", + "sv": "Redakt\u00f6r", + "und": "Julkaisutoimittaja" + } } ], "email": "rahoitus@rahaorg.fi", @@ -7073,7 +7555,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Hosting Institution", + "fi": "S\u00e4ilytt\u00e4v\u00e4 organisaatio", + "sv": "Bevarande organisation", + "und": "S\u00e4ilytt\u00e4v\u00e4 organisaatio" + } } ], "email": "info@csc.fi", @@ -7097,7 +7586,13 @@ ], "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9611", @@ -7110,7 +7605,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Point of contact", + "fi": "Yhteystiedot", + "sv": "Kontaktuppgifter", + "und": "Yhteystiedot" + } } ], "email": "info@csc.fi", @@ -7141,7 +7643,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Collector", + "fi": "Aineiston ker\u00e4\u00e4j\u00e4", + "sv": "Datainsamling", + "und": "Aineiston ker\u00e4\u00e4j\u00e4" + } } ], "email": "info@csc.fi", @@ -7173,14 +7682,26 @@ "en": "Description of provenance activity" }, "event_outcome": { - "identifier": "Success" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/success", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Success", + "fi": "Onnistunut", + "sv": "Framg\u00e5ngsrik", + "und": "Onnistunut" + } }, "lifecycle_event": { "definition": { "en": "A statement or formal explanation of the meaning of a concept." }, "identifier": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", - "in_scheme": "http://uri.of.filetype.concept/scheme" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event", + "pref_label": { + "en": "Collected", + "fi": "Ker\u00e4tty", + "und": "Ker\u00e4tty" + } }, "outcome_description": { "en": "A grand success", @@ -7191,7 +7712,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, "temporal": { @@ -7258,7 +7786,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Curator", + "fi": "Aineiston k\u00e4sittelij\u00e4", + "sv": "Databehandling", + "und": "Aineiston k\u00e4sittelij\u00e4" + } } ], "email": "info@csc.fi", @@ -7278,21 +7813,41 @@ "en": "Description of other provenance activity" }, "event_outcome": { - "identifier": "Failure" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/failure", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Failure", + "fi": "Ep\u00e4onnistunut", + "sv": "Misslyckad", + "und": "Ep\u00e4onnistunut" + } }, "outcome_description": { "en": "A grand failure", "fi": "Ep\u00e4onnistui" }, "preservation_event": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd" + "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/preservation_event", + "pref_label": { + "en": "Object update", + "fi": "Objektin p\u00e4ivitys", + "und": "Objektin p\u00e4ivitys" + } }, "spatial": { "alt": "11.111", "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p105917" + "identifier": "http://www.yso.fi/onto/yso/p105917", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Southern Finland Province", + "fi": "Etel\u00e4-Suomi", + "sv": "S\u00f6dra Finland", + "und": "Etel\u00e4-Suomi" + } } }, "temporal": { @@ -7308,7 +7863,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } } ], "email": "info@csc.fi", @@ -7359,11 +7921,22 @@ "fi": "Ulkopuolinen aineisto 1" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Physical object", + "fi": "Fyysinen objekti", + "und": "Fyysinen objekti" + } } }, "relation_type": { - "identifier": "wasDerivedFrom" + "identifier": "http://www.w3.org/ns/prov#wasDerivedFrom", + "pref_label": { + "en": "Was derived from", + "fi": "Johdettu aineistosta", + "und": "Johdettu aineistosta" + } } }, { @@ -7377,11 +7950,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Collection", + "fi": "Kokoelma", + "und": "Kokoelma" + } } }, "relation_type": { - "identifier": "cites" + "identifier": "http://purl.org/spar/cito/cites", + "pref_label": { + "en": "Cites", + "fi": "Viittaa", + "und": "Viittaa" + } } }, { @@ -7395,11 +7979,22 @@ "fi": "Ulkopuolinen aineisto 2" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Service", + "fi": "Palvelu", + "und": "Palvelu" + } } }, "relation_type": { - "identifier": "isPartOf" + "identifier": "http://purl.org/dc/terms/isPartOf", + "pref_label": { + "en": "Is part of", + "fi": "Kuuluu aineistoon", + "und": "Kuuluu aineistoon" + } } }, { @@ -7413,11 +8008,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "User interface", + "fi": "K\u00e4ytt\u00f6liittym\u00e4", + "und": "K\u00e4ytt\u00f6liittym\u00e4" + } } }, "relation_type": { - "identifier": "hasPreviousVersion" + "identifier": "http://www.w3.org/ns/adms#previous", + "pref_label": { + "en": "Has previous version", + "fi": "Edellinen versio", + "und": "Edellinen versio" + } } } ], @@ -7426,7 +8032,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Manager", + "fi": "Aineiston hallinnoija", + "sv": "Dataadministration", + "und": "Aineiston hallinnoija" + } } ], "email": "info@csc.fi", @@ -7457,7 +8070,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, { @@ -7465,7 +8085,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name 2", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } } ], @@ -7525,12 +8152,6 @@ "dataset_version_set": 12, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", - "editor": { - "creator_id": "053d18ecb29e752cb7a35cd77b34f5fd", - "identifier": "qvain", - "owner_id": "053d18ecb29e752cb7a35cd77b34f5fd", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9012" - }, "files": [ 1, 2, @@ -7560,13 +8181,12 @@ "research_dataset": { "access_rights": { "access_type": { - "definition": { - "en": "A statement or formal explanation of the meaning of a concept." - }, "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", - "in_scheme": "http://uri.of.filetype.concept/scheme", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", "pref_label": { - "en": "pref label for this type" + "en": "Open", + "fi": "Avoin", + "und": "Avoin" } }, "access_url": { @@ -7590,7 +8210,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } }, { @@ -7598,20 +8219,32 @@ "en": "Free account of the rights" }, "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-2.0", + "license": "https://creativecommons.org/licenses/by-nc/2.0/", "title": { - "en": "A name given to the resource" + "en": "Creative Commons Attribution-NonCommercial 2.0 Generic (CC BY-NC 2.0", + "fi": "Creative Commons Nime\u00e4-EiKaupallinen 2.0 Yleinen (CC BY-NC 2.0)", + "und": "Creative Commons Nime\u00e4-EiKaupallinen 2.0 Yleinen (CC BY-NC 2.0)" } }, { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/other", "title": { - "en": "A name given to the resource" + "en": "Other", + "fi": "Muu", + "und": "Muu" } } ], "restriction_grounds": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other" + "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds", + "pref_label": { + "en": "Restricted access due to other reasons", + "fi": "Saatavuutta rajoitettu muulla perusteella", + "sv": "Begr\u00e4nsad \u00e5tkomst av \u00f6vriga sk\u00e4l", + "und": "Saatavuutta rajoitettu muulla perusteella" + } } ] }, @@ -7621,7 +8254,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Funding acquisition", + "fi": "Rahoituksen hankinta", + "sv": "Anskaffande av finansiering", + "und": "Rahoituksen hankinta" + } } ], "email": "kalle.kontribuuttaaja@csc.fi", @@ -7647,7 +8287,14 @@ "@type": "Person", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Project Leader", + "fi": "Projektin johtaja", + "sv": "Projektledare", + "und": "Projektin johtaja" + } } ], "email": "franzibald.kontribuuttaaja@csc.fi", @@ -7675,7 +8322,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Conceptualization", + "fi": "Tutkimuksen muotoilu", + "sv": "Formulering av forskningen", + "und": "Tutkimuksen muotoilu" + } } ], "member_of": { @@ -7694,10 +8348,24 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } }, { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Sponsor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Sponsor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Sponsor", + "fi": "Sponsori", + "sv": "Sponsor", + "und": "Sponsori" + } } ], "identifier": "id:of:curator:default", @@ -7782,13 +8450,25 @@ ], "infrastructure": [ { - "identifier": "urn-nbn-fi-research-infras-2016072530" + "identifier": "urn-nbn-fi-research-infras-2016072530", + "in_scheme": "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures", + "pref_label": { + "en": "Accelerator laboratory of the university of Jyv\u00e4skyl\u00e4", + "fi": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio", + "und": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio" + } } ], "is_output_of": [ { "funder_type": { - "identifier": "tekes" + "identifier": "http://uri.suomi.fi/codelist/fairdata/funder_type/code/tekes", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/funder_type", + "pref_label": { + "en": "Tekes", + "fi": "Tekes", + "und": "Tekes" + } }, "has_funder_identifier": "funderprojectidentifier", "has_funding_agency": [ @@ -7796,7 +8476,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Editor", + "fi": "Julkaisutoimittaja", + "sv": "Redakt\u00f6r", + "und": "Julkaisutoimittaja" + } } ], "email": "rahoitus@rahaorg.fi", @@ -7819,7 +8506,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Hosting Institution", + "fi": "S\u00e4ilytt\u00e4v\u00e4 organisaatio", + "sv": "Bevarande organisation", + "und": "S\u00e4ilytt\u00e4v\u00e4 organisaatio" + } } ], "email": "info@csc.fi", @@ -7843,7 +8537,13 @@ ], "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9612", @@ -7856,7 +8556,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Point of contact", + "fi": "Yhteystiedot", + "sv": "Kontaktuppgifter", + "und": "Yhteystiedot" + } } ], "email": "info@csc.fi", @@ -7887,7 +8594,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Collector", + "fi": "Aineiston ker\u00e4\u00e4j\u00e4", + "sv": "Datainsamling", + "und": "Aineiston ker\u00e4\u00e4j\u00e4" + } } ], "email": "info@csc.fi", @@ -7919,14 +8633,26 @@ "en": "Description of provenance activity" }, "event_outcome": { - "identifier": "Success" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/success", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Success", + "fi": "Onnistunut", + "sv": "Framg\u00e5ngsrik", + "und": "Onnistunut" + } }, "lifecycle_event": { "definition": { "en": "A statement or formal explanation of the meaning of a concept." }, "identifier": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", - "in_scheme": "http://uri.of.filetype.concept/scheme" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event", + "pref_label": { + "en": "Collected", + "fi": "Ker\u00e4tty", + "und": "Ker\u00e4tty" + } }, "outcome_description": { "en": "A grand success", @@ -7937,7 +8663,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, "temporal": { @@ -8004,7 +8737,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Curator", + "fi": "Aineiston k\u00e4sittelij\u00e4", + "sv": "Databehandling", + "und": "Aineiston k\u00e4sittelij\u00e4" + } } ], "email": "info@csc.fi", @@ -8024,21 +8764,41 @@ "en": "Description of other provenance activity" }, "event_outcome": { - "identifier": "Failure" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/failure", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Failure", + "fi": "Ep\u00e4onnistunut", + "sv": "Misslyckad", + "und": "Ep\u00e4onnistunut" + } }, "outcome_description": { "en": "A grand failure", "fi": "Ep\u00e4onnistui" }, "preservation_event": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd" + "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/preservation_event", + "pref_label": { + "en": "Object update", + "fi": "Objektin p\u00e4ivitys", + "und": "Objektin p\u00e4ivitys" + } }, "spatial": { "alt": "11.111", "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p105917" + "identifier": "http://www.yso.fi/onto/yso/p105917", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Southern Finland Province", + "fi": "Etel\u00e4-Suomi", + "sv": "S\u00f6dra Finland", + "und": "Etel\u00e4-Suomi" + } } }, "temporal": { @@ -8054,7 +8814,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } } ], "email": "info@csc.fi", @@ -8105,11 +8872,22 @@ "fi": "Ulkopuolinen aineisto 1" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Physical object", + "fi": "Fyysinen objekti", + "und": "Fyysinen objekti" + } } }, "relation_type": { - "identifier": "wasDerivedFrom" + "identifier": "http://www.w3.org/ns/prov#wasDerivedFrom", + "pref_label": { + "en": "Was derived from", + "fi": "Johdettu aineistosta", + "und": "Johdettu aineistosta" + } } }, { @@ -8123,11 +8901,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Collection", + "fi": "Kokoelma", + "und": "Kokoelma" + } } }, "relation_type": { - "identifier": "cites" + "identifier": "http://purl.org/spar/cito/cites", + "pref_label": { + "en": "Cites", + "fi": "Viittaa", + "und": "Viittaa" + } } }, { @@ -8141,11 +8930,22 @@ "fi": "Ulkopuolinen aineisto 2" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Service", + "fi": "Palvelu", + "und": "Palvelu" + } } }, "relation_type": { - "identifier": "isPartOf" + "identifier": "http://purl.org/dc/terms/isPartOf", + "pref_label": { + "en": "Is part of", + "fi": "Kuuluu aineistoon", + "und": "Kuuluu aineistoon" + } } }, { @@ -8159,11 +8959,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "User interface", + "fi": "K\u00e4ytt\u00f6liittym\u00e4", + "und": "K\u00e4ytt\u00f6liittym\u00e4" + } } }, "relation_type": { - "identifier": "hasPreviousVersion" + "identifier": "http://www.w3.org/ns/adms#previous", + "pref_label": { + "en": "Has previous version", + "fi": "Edellinen versio", + "und": "Edellinen versio" + } } } ], @@ -8172,7 +8983,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Manager", + "fi": "Aineiston hallinnoija", + "sv": "Dataadministration", + "und": "Aineiston hallinnoija" + } } ], "email": "info@csc.fi", @@ -8203,7 +9021,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, { @@ -8211,7 +9036,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name 2", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } } ], @@ -8271,12 +9103,6 @@ "dataset_version_set": 13, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", - "editor": { - "creator_id": "05593961536b76fa825281ccaedd4d4f", - "identifier": "qvain", - "owner_id": "05593961536b76fa825281ccaedd4d4f", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9013" - }, "files": [ 22, 23, @@ -8374,13 +9200,12 @@ "research_dataset": { "access_rights": { "access_type": { - "definition": { - "en": "A statement or formal explanation of the meaning of a concept." - }, "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", - "in_scheme": "http://uri.of.filetype.concept/scheme", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", "pref_label": { - "en": "pref label for this type" + "en": "Open", + "fi": "Avoin", + "und": "Avoin" } }, "access_url": { @@ -8404,7 +9229,8 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } }, { @@ -8412,20 +9238,32 @@ "en": "Free account of the rights" }, "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-NC-2.0", + "license": "https://creativecommons.org/licenses/by-nc/2.0/", "title": { - "en": "A name given to the resource" + "en": "Creative Commons Attribution-NonCommercial 2.0 Generic (CC BY-NC 2.0", + "fi": "Creative Commons Nime\u00e4-EiKaupallinen 2.0 Yleinen (CC BY-NC 2.0)", + "und": "Creative Commons Nime\u00e4-EiKaupallinen 2.0 Yleinen (CC BY-NC 2.0)" } }, { "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/other", "title": { - "en": "A name given to the resource" + "en": "Other", + "fi": "Muu", + "und": "Muu" } } ], "restriction_grounds": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other" + "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds", + "pref_label": { + "en": "Restricted access due to other reasons", + "fi": "Saatavuutta rajoitettu muulla perusteella", + "sv": "Begr\u00e4nsad \u00e5tkomst av \u00f6vriga sk\u00e4l", + "und": "Saatavuutta rajoitettu muulla perusteella" + } } ] }, @@ -8435,7 +9273,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Funding acquisition", + "fi": "Rahoituksen hankinta", + "sv": "Anskaffande av finansiering", + "und": "Rahoituksen hankinta" + } } ], "email": "kalle.kontribuuttaaja@csc.fi", @@ -8461,7 +9306,14 @@ "@type": "Person", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Project Leader", + "fi": "Projektin johtaja", + "sv": "Projektledare", + "und": "Projektin johtaja" + } } ], "email": "franzibald.kontribuuttaaja@csc.fi", @@ -8489,7 +9341,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Conceptualization", + "fi": "Tutkimuksen muotoilu", + "sv": "Formulering av forskningen", + "und": "Tutkimuksen muotoilu" + } } ], "member_of": { @@ -8508,10 +9367,24 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } }, { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Sponsor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Sponsor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Sponsor", + "fi": "Sponsori", + "sv": "Sponsor", + "und": "Sponsori" + } } ], "identifier": "id:of:curator:default", @@ -8620,13 +9493,25 @@ ], "infrastructure": [ { - "identifier": "urn-nbn-fi-research-infras-2016072530" + "identifier": "urn-nbn-fi-research-infras-2016072530", + "in_scheme": "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures", + "pref_label": { + "en": "Accelerator laboratory of the university of Jyv\u00e4skyl\u00e4", + "fi": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio", + "und": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio" + } } ], "is_output_of": [ { "funder_type": { - "identifier": "tekes" + "identifier": "http://uri.suomi.fi/codelist/fairdata/funder_type/code/tekes", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/funder_type", + "pref_label": { + "en": "Tekes", + "fi": "Tekes", + "und": "Tekes" + } }, "has_funder_identifier": "funderprojectidentifier", "has_funding_agency": [ @@ -8634,7 +9519,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Editor", + "fi": "Julkaisutoimittaja", + "sv": "Redakt\u00f6r", + "und": "Julkaisutoimittaja" + } } ], "email": "rahoitus@rahaorg.fi", @@ -8657,7 +9549,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Hosting Institution", + "fi": "S\u00e4ilytt\u00e4v\u00e4 organisaatio", + "sv": "Bevarande organisation", + "und": "S\u00e4ilytt\u00e4v\u00e4 organisaatio" + } } ], "email": "info@csc.fi", @@ -8681,7 +9580,13 @@ ], "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9613", @@ -8694,7 +9599,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Point of contact", + "fi": "Yhteystiedot", + "sv": "Kontaktuppgifter", + "und": "Yhteystiedot" + } } ], "email": "info@csc.fi", @@ -8725,7 +9637,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Collector", + "fi": "Aineiston ker\u00e4\u00e4j\u00e4", + "sv": "Datainsamling", + "und": "Aineiston ker\u00e4\u00e4j\u00e4" + } } ], "email": "info@csc.fi", @@ -8757,14 +9676,26 @@ "en": "Description of provenance activity" }, "event_outcome": { - "identifier": "Success" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/success", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Success", + "fi": "Onnistunut", + "sv": "Framg\u00e5ngsrik", + "und": "Onnistunut" + } }, "lifecycle_event": { "definition": { "en": "A statement or formal explanation of the meaning of a concept." }, "identifier": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", - "in_scheme": "http://uri.of.filetype.concept/scheme" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event", + "pref_label": { + "en": "Collected", + "fi": "Ker\u00e4tty", + "und": "Ker\u00e4tty" + } }, "outcome_description": { "en": "A grand success", @@ -8775,7 +9706,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, "temporal": { @@ -8842,7 +9780,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Curator", + "fi": "Aineiston k\u00e4sittelij\u00e4", + "sv": "Databehandling", + "und": "Aineiston k\u00e4sittelij\u00e4" + } } ], "email": "info@csc.fi", @@ -8862,21 +9807,41 @@ "en": "Description of other provenance activity" }, "event_outcome": { - "identifier": "Failure" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/failure", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Failure", + "fi": "Ep\u00e4onnistunut", + "sv": "Misslyckad", + "und": "Ep\u00e4onnistunut" + } }, "outcome_description": { "en": "A grand failure", "fi": "Ep\u00e4onnistui" }, "preservation_event": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd" + "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/preservation_event", + "pref_label": { + "en": "Object update", + "fi": "Objektin p\u00e4ivitys", + "und": "Objektin p\u00e4ivitys" + } }, "spatial": { "alt": "11.111", "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p105917" + "identifier": "http://www.yso.fi/onto/yso/p105917", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Southern Finland Province", + "fi": "Etel\u00e4-Suomi", + "sv": "S\u00f6dra Finland", + "und": "Etel\u00e4-Suomi" + } } }, "temporal": { @@ -8892,7 +9857,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } } ], "email": "info@csc.fi", @@ -8943,11 +9915,22 @@ "fi": "Ulkopuolinen aineisto 1" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Physical object", + "fi": "Fyysinen objekti", + "und": "Fyysinen objekti" + } } }, "relation_type": { - "identifier": "wasDerivedFrom" + "identifier": "http://www.w3.org/ns/prov#wasDerivedFrom", + "pref_label": { + "en": "Was derived from", + "fi": "Johdettu aineistosta", + "und": "Johdettu aineistosta" + } } }, { @@ -8961,11 +9944,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Collection", + "fi": "Kokoelma", + "und": "Kokoelma" + } } }, "relation_type": { - "identifier": "cites" + "identifier": "http://purl.org/spar/cito/cites", + "pref_label": { + "en": "Cites", + "fi": "Viittaa", + "und": "Viittaa" + } } }, { @@ -8979,11 +9973,22 @@ "fi": "Ulkopuolinen aineisto 2" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Service", + "fi": "Palvelu", + "und": "Palvelu" + } } }, "relation_type": { - "identifier": "isPartOf" + "identifier": "http://purl.org/dc/terms/isPartOf", + "pref_label": { + "en": "Is part of", + "fi": "Kuuluu aineistoon", + "und": "Kuuluu aineistoon" + } } }, { @@ -8997,11 +10002,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "User interface", + "fi": "K\u00e4ytt\u00f6liittym\u00e4", + "und": "K\u00e4ytt\u00f6liittym\u00e4" + } } }, "relation_type": { - "identifier": "hasPreviousVersion" + "identifier": "http://www.w3.org/ns/adms#previous", + "pref_label": { + "en": "Has previous version", + "fi": "Edellinen versio", + "und": "Edellinen versio" + } } } ], @@ -9010,7 +10026,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Manager", + "fi": "Aineiston hallinnoija", + "sv": "Dataadministration", + "und": "Aineiston hallinnoija" + } } ], "email": "info@csc.fi", @@ -9041,7 +10064,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, { @@ -9049,7 +10079,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name 2", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } } ], @@ -9120,7 +10157,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -9128,7 +10171,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -9145,7 +10195,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "ResearchGroup", + "fi": "Tutkimusryhm\u00e4", + "sv": "Forskningsgrupp", + "und": "Tutkimusryhm\u00e4" + } } ], "identifier": "10076-E700", @@ -9161,7 +10218,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9614", @@ -9233,7 +10296,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -9241,7 +10310,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -9258,7 +10334,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "ResearchGroup", + "fi": "Tutkimusryhm\u00e4", + "sv": "Forskningsgrupp", + "und": "Tutkimusryhm\u00e4" + } } ], "identifier": "10076-E700", @@ -9274,7 +10357,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9615", @@ -9346,7 +10435,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -9354,7 +10449,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -9371,7 +10473,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "ResearchGroup", + "fi": "Tutkimusryhm\u00e4", + "sv": "Forskningsgrupp", + "und": "Tutkimusryhm\u00e4" + } } ], "identifier": "10076-E700", @@ -9387,7 +10496,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9616", @@ -9459,7 +10574,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -9467,7 +10588,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -9484,7 +10612,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "ResearchGroup", + "fi": "Tutkimusryhm\u00e4", + "sv": "Forskningsgrupp", + "und": "Tutkimusryhm\u00e4" + } } ], "identifier": "10076-E700", @@ -9500,7 +10635,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9617", @@ -9572,7 +10713,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -9580,7 +10727,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -9597,7 +10751,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "ResearchGroup", + "fi": "Tutkimusryhm\u00e4", + "sv": "Forskningsgrupp", + "und": "Tutkimusryhm\u00e4" + } } ], "identifier": "10076-E700", @@ -9613,7 +10774,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9618", @@ -9685,7 +10852,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -9693,7 +10866,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -9710,7 +10890,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ResearchGroup", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "ResearchGroup", + "fi": "Tutkimusryhm\u00e4", + "sv": "Forskningsgrupp", + "und": "Tutkimusryhm\u00e4" + } } ], "identifier": "10076-E700", @@ -9726,7 +10913,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9619", @@ -9798,7 +10991,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -9806,7 +11005,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -9837,7 +11043,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9620", @@ -9909,7 +11121,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -9917,7 +11135,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -9948,7 +11173,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9621", @@ -10020,7 +11251,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -10028,7 +11265,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -10059,7 +11303,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9622", @@ -10131,7 +11381,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -10139,7 +11395,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -10170,7 +11433,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9623", @@ -10230,10 +11499,6 @@ "data_catalog": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", - "editor": { - "creator_id": "053bffbcc41edad4853bea91fc42ea18", - "owner_id": "053bffbcc41edad4853bea91fc42ea18" - }, "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9624", "metadata_owner_org": "abc-org-123", "metadata_provider_org": "abc-org-123", @@ -10241,13 +11506,12 @@ "research_dataset": { "access_rights": { "access_type": { - "definition": { - "en": "A statement or formal explanation of the meaning of a concept." - }, "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", - "in_scheme": "http://uri.of.filetype.concept/scheme", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", "pref_label": { - "en": "pref label for this type" + "en": "Open", + "fi": "Avoin", + "und": "Avoin" } }, "access_url": { @@ -10271,13 +11535,21 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } } ], "restriction_grounds": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other" + "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds", + "pref_label": { + "en": "Restricted access due to other reasons", + "fi": "Saatavuutta rajoitettu muulla perusteella", + "sv": "Begr\u00e4nsad \u00e5tkomst av \u00f6vriga sk\u00e4l", + "und": "Saatavuutta rajoitettu muulla perusteella" + } } ] }, @@ -10287,7 +11559,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Funding acquisition", + "fi": "Rahoituksen hankinta", + "sv": "Anskaffande av finansiering", + "und": "Rahoituksen hankinta" + } } ], "email": "kalle.kontribuuttaaja@csc.fi", @@ -10313,7 +11592,14 @@ "@type": "Person", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Project Leader", + "fi": "Projektin johtaja", + "sv": "Projektledare", + "und": "Projektin johtaja" + } } ], "email": "franzibald.kontribuuttaaja@csc.fi", @@ -10341,7 +11627,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Conceptualization", + "fi": "Tutkimuksen muotoilu", + "sv": "Formulering av forskningen", + "und": "Tutkimuksen muotoilu" + } } ], "member_of": { @@ -10360,7 +11653,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } } ], "identifier": "id:of:curator:default", @@ -10395,13 +11695,25 @@ ], "infrastructure": [ { - "identifier": "urn-nbn-fi-research-infras-2016072530" + "identifier": "urn-nbn-fi-research-infras-2016072530", + "in_scheme": "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures", + "pref_label": { + "en": "Accelerator laboratory of the university of Jyv\u00e4skyl\u00e4", + "fi": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio", + "und": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio" + } } ], "is_output_of": [ { "funder_type": { - "identifier": "tekes" + "identifier": "http://uri.suomi.fi/codelist/fairdata/funder_type/code/tekes", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/funder_type", + "pref_label": { + "en": "Tekes", + "fi": "Tekes", + "und": "Tekes" + } }, "has_funder_identifier": "funderprojectidentifier", "has_funding_agency": [ @@ -10409,7 +11721,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Editor", + "fi": "Julkaisutoimittaja", + "sv": "Redakt\u00f6r", + "und": "Julkaisutoimittaja" + } } ], "email": "rahoitus@rahaorg.fi", @@ -10432,7 +11751,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Hosting Institution", + "fi": "S\u00e4ilytt\u00e4v\u00e4 organisaatio", + "sv": "Bevarande organisation", + "und": "S\u00e4ilytt\u00e4v\u00e4 organisaatio" + } } ], "email": "info@csc.fi", @@ -10456,7 +11782,13 @@ ], "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9624", @@ -10469,7 +11801,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Point of contact", + "fi": "Yhteystiedot", + "sv": "Kontaktuppgifter", + "und": "Yhteystiedot" + } } ], "email": "info@csc.fi", @@ -10500,7 +11839,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Collector", + "fi": "Aineiston ker\u00e4\u00e4j\u00e4", + "sv": "Datainsamling", + "und": "Aineiston ker\u00e4\u00e4j\u00e4" + } } ], "email": "info@csc.fi", @@ -10532,14 +11878,26 @@ "en": "Description of provenance activity" }, "event_outcome": { - "identifier": "Success" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/success", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Success", + "fi": "Onnistunut", + "sv": "Framg\u00e5ngsrik", + "und": "Onnistunut" + } }, "lifecycle_event": { "definition": { "en": "A statement or formal explanation of the meaning of a concept." }, "identifier": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", - "in_scheme": "http://uri.of.filetype.concept/scheme" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event", + "pref_label": { + "en": "Collected", + "fi": "Ker\u00e4tty", + "und": "Ker\u00e4tty" + } }, "outcome_description": { "en": "A grand success", @@ -10550,7 +11908,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, "temporal": { @@ -10617,7 +11982,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Curator", + "fi": "Aineiston k\u00e4sittelij\u00e4", + "sv": "Databehandling", + "und": "Aineiston k\u00e4sittelij\u00e4" + } } ], "email": "info@csc.fi", @@ -10637,21 +12009,41 @@ "en": "Description of other provenance activity" }, "event_outcome": { - "identifier": "Failure" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/failure", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Failure", + "fi": "Ep\u00e4onnistunut", + "sv": "Misslyckad", + "und": "Ep\u00e4onnistunut" + } }, "outcome_description": { "en": "A grand failure", "fi": "Ep\u00e4onnistui" }, "preservation_event": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd" + "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/preservation_event", + "pref_label": { + "en": "Object update", + "fi": "Objektin p\u00e4ivitys", + "und": "Objektin p\u00e4ivitys" + } }, "spatial": { "alt": "11.111", "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p105917" + "identifier": "http://www.yso.fi/onto/yso/p105917", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Southern Finland Province", + "fi": "Etel\u00e4-Suomi", + "sv": "S\u00f6dra Finland", + "und": "Etel\u00e4-Suomi" + } } }, "temporal": { @@ -10667,7 +12059,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } } ], "email": "info@csc.fi", @@ -10718,11 +12117,22 @@ "fi": "Ulkopuolinen aineisto 1" }, "type": { - "identifier": "physical_object" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Physical object", + "fi": "Fyysinen objekti", + "und": "Fyysinen objekti" + } } }, "relation_type": { - "identifier": "wasDerivedFrom" + "identifier": "http://www.w3.org/ns/prov#wasDerivedFrom", + "pref_label": { + "en": "Was derived from", + "fi": "Johdettu aineistosta", + "und": "Johdettu aineistosta" + } } }, { @@ -10736,11 +12146,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "collection" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Collection", + "fi": "Kokoelma", + "und": "Kokoelma" + } } }, "relation_type": { - "identifier": "cites" + "identifier": "http://purl.org/spar/cito/cites", + "pref_label": { + "en": "Cites", + "fi": "Viittaa", + "und": "Viittaa" + } } }, { @@ -10754,11 +12175,22 @@ "fi": "Ulkopuolinen aineisto 2" }, "type": { - "identifier": "service" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Service", + "fi": "Palvelu", + "und": "Palvelu" + } } }, "relation_type": { - "identifier": "isPartOf" + "identifier": "http://purl.org/dc/terms/isPartOf", + "pref_label": { + "en": "Is part of", + "fi": "Kuuluu aineistoon", + "und": "Kuuluu aineistoon" + } } }, { @@ -10772,11 +12204,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "ui" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "User interface", + "fi": "K\u00e4ytt\u00f6liittym\u00e4", + "und": "K\u00e4ytt\u00f6liittym\u00e4" + } } }, "relation_type": { - "identifier": "hasPreviousVersion" + "identifier": "http://www.w3.org/ns/adms#previous", + "pref_label": { + "en": "Has previous version", + "fi": "Edellinen versio", + "und": "Edellinen versio" + } } } ], @@ -10817,9 +12260,10 @@ "en": "Free account of the rights" }, "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", - "license": "https://url.of.license.which.applies.org", + "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } } ], @@ -10853,9 +12297,10 @@ "en": "Free account of the rights" }, "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", - "license": "https://url.of.license.which.applies.org", + "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } } ], @@ -10879,7 +12324,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Manager", + "fi": "Aineiston hallinnoija", + "sv": "Dataadministration", + "und": "Aineiston hallinnoija" + } } ], "email": "info@csc.fi", @@ -10910,7 +12362,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, { @@ -10918,7 +12377,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name 2", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } } ], @@ -10967,10 +12433,6 @@ "data_catalog": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", - "editor": { - "creator_id": "053bffbcc41edad4853bea91fc42ea18", - "owner_id": "053d18ecb29e752cb7a35cd77b34f5fd" - }, "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9625", "metadata_owner_org": "abc-org-123", "metadata_provider_org": "abc-org-123", @@ -10978,13 +12440,12 @@ "research_dataset": { "access_rights": { "access_type": { - "definition": { - "en": "A statement or formal explanation of the meaning of a concept." - }, "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", - "in_scheme": "http://uri.of.filetype.concept/scheme", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", "pref_label": { - "en": "pref label for this type" + "en": "Open", + "fi": "Avoin", + "und": "Avoin" } }, "access_url": { @@ -11008,13 +12469,21 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } } ], "restriction_grounds": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other" + "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds", + "pref_label": { + "en": "Restricted access due to other reasons", + "fi": "Saatavuutta rajoitettu muulla perusteella", + "sv": "Begr\u00e4nsad \u00e5tkomst av \u00f6vriga sk\u00e4l", + "und": "Saatavuutta rajoitettu muulla perusteella" + } } ] }, @@ -11024,7 +12493,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Funding acquisition", + "fi": "Rahoituksen hankinta", + "sv": "Anskaffande av finansiering", + "und": "Rahoituksen hankinta" + } } ], "email": "kalle.kontribuuttaaja@csc.fi", @@ -11050,7 +12526,14 @@ "@type": "Person", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Project Leader", + "fi": "Projektin johtaja", + "sv": "Projektledare", + "und": "Projektin johtaja" + } } ], "email": "franzibald.kontribuuttaaja@csc.fi", @@ -11078,7 +12561,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Conceptualization", + "fi": "Tutkimuksen muotoilu", + "sv": "Formulering av forskningen", + "und": "Tutkimuksen muotoilu" + } } ], "member_of": { @@ -11097,7 +12587,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } } ], "identifier": "id:of:curator:default", @@ -11132,13 +12629,25 @@ ], "infrastructure": [ { - "identifier": "urn-nbn-fi-research-infras-2016072530" + "identifier": "urn-nbn-fi-research-infras-2016072530", + "in_scheme": "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures", + "pref_label": { + "en": "Accelerator laboratory of the university of Jyv\u00e4skyl\u00e4", + "fi": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio", + "und": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio" + } } ], "is_output_of": [ { "funder_type": { - "identifier": "tekes" + "identifier": "http://uri.suomi.fi/codelist/fairdata/funder_type/code/tekes", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/funder_type", + "pref_label": { + "en": "Tekes", + "fi": "Tekes", + "und": "Tekes" + } }, "has_funder_identifier": "funderprojectidentifier", "has_funding_agency": [ @@ -11146,7 +12655,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Editor", + "fi": "Julkaisutoimittaja", + "sv": "Redakt\u00f6r", + "und": "Julkaisutoimittaja" + } } ], "email": "rahoitus@rahaorg.fi", @@ -11169,7 +12685,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Hosting Institution", + "fi": "S\u00e4ilytt\u00e4v\u00e4 organisaatio", + "sv": "Bevarande organisation", + "und": "S\u00e4ilytt\u00e4v\u00e4 organisaatio" + } } ], "email": "info@csc.fi", @@ -11193,7 +12716,13 @@ ], "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9625", @@ -11206,7 +12735,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Point of contact", + "fi": "Yhteystiedot", + "sv": "Kontaktuppgifter", + "und": "Yhteystiedot" + } } ], "email": "info@csc.fi", @@ -11237,7 +12773,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Collector", + "fi": "Aineiston ker\u00e4\u00e4j\u00e4", + "sv": "Datainsamling", + "und": "Aineiston ker\u00e4\u00e4j\u00e4" + } } ], "email": "info@csc.fi", @@ -11269,14 +12812,26 @@ "en": "Description of provenance activity" }, "event_outcome": { - "identifier": "Success" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/success", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Success", + "fi": "Onnistunut", + "sv": "Framg\u00e5ngsrik", + "und": "Onnistunut" + } }, "lifecycle_event": { "definition": { "en": "A statement or formal explanation of the meaning of a concept." }, "identifier": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", - "in_scheme": "http://uri.of.filetype.concept/scheme" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event", + "pref_label": { + "en": "Collected", + "fi": "Ker\u00e4tty", + "und": "Ker\u00e4tty" + } }, "outcome_description": { "en": "A grand success", @@ -11287,7 +12842,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, "temporal": { @@ -11354,7 +12916,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Curator", + "fi": "Aineiston k\u00e4sittelij\u00e4", + "sv": "Databehandling", + "und": "Aineiston k\u00e4sittelij\u00e4" + } } ], "email": "info@csc.fi", @@ -11374,21 +12943,41 @@ "en": "Description of other provenance activity" }, "event_outcome": { - "identifier": "Failure" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/failure", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Failure", + "fi": "Ep\u00e4onnistunut", + "sv": "Misslyckad", + "und": "Ep\u00e4onnistunut" + } }, "outcome_description": { "en": "A grand failure", "fi": "Ep\u00e4onnistui" }, "preservation_event": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd" + "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/preservation_event", + "pref_label": { + "en": "Object update", + "fi": "Objektin p\u00e4ivitys", + "und": "Objektin p\u00e4ivitys" + } }, "spatial": { "alt": "11.111", "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p105917" + "identifier": "http://www.yso.fi/onto/yso/p105917", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Southern Finland Province", + "fi": "Etel\u00e4-Suomi", + "sv": "S\u00f6dra Finland", + "und": "Etel\u00e4-Suomi" + } } }, "temporal": { @@ -11404,7 +12993,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } } ], "email": "info@csc.fi", @@ -11455,11 +13051,22 @@ "fi": "Ulkopuolinen aineisto 1" }, "type": { - "identifier": "physical_object" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Physical object", + "fi": "Fyysinen objekti", + "und": "Fyysinen objekti" + } } }, "relation_type": { - "identifier": "wasDerivedFrom" + "identifier": "http://www.w3.org/ns/prov#wasDerivedFrom", + "pref_label": { + "en": "Was derived from", + "fi": "Johdettu aineistosta", + "und": "Johdettu aineistosta" + } } }, { @@ -11473,11 +13080,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "collection" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Collection", + "fi": "Kokoelma", + "und": "Kokoelma" + } } }, "relation_type": { - "identifier": "cites" + "identifier": "http://purl.org/spar/cito/cites", + "pref_label": { + "en": "Cites", + "fi": "Viittaa", + "und": "Viittaa" + } } }, { @@ -11491,11 +13109,22 @@ "fi": "Ulkopuolinen aineisto 2" }, "type": { - "identifier": "service" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Service", + "fi": "Palvelu", + "und": "Palvelu" + } } }, "relation_type": { - "identifier": "isPartOf" + "identifier": "http://purl.org/dc/terms/isPartOf", + "pref_label": { + "en": "Is part of", + "fi": "Kuuluu aineistoon", + "und": "Kuuluu aineistoon" + } } }, { @@ -11509,11 +13138,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "ui" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "User interface", + "fi": "K\u00e4ytt\u00f6liittym\u00e4", + "und": "K\u00e4ytt\u00f6liittym\u00e4" + } } }, "relation_type": { - "identifier": "hasPreviousVersion" + "identifier": "http://www.w3.org/ns/adms#previous", + "pref_label": { + "en": "Has previous version", + "fi": "Edellinen versio", + "und": "Edellinen versio" + } } } ], @@ -11554,9 +13194,10 @@ "en": "Free account of the rights" }, "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", - "license": "https://url.of.license.which.applies.org", + "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } } ], @@ -11590,9 +13231,10 @@ "en": "Free account of the rights" }, "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", - "license": "https://url.of.license.which.applies.org", + "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } } ], @@ -11616,7 +13258,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Manager", + "fi": "Aineiston hallinnoija", + "sv": "Dataadministration", + "und": "Aineiston hallinnoija" + } } ], "email": "info@csc.fi", @@ -11647,7 +13296,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, { @@ -11655,7 +13311,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name 2", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } } ], @@ -11704,10 +13367,6 @@ "data_catalog": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", - "editor": { - "creator_id": "053bffbcc41edad4853bea91fc42ea18", - "owner_id": "05593961536b76fa825281ccaedd4d4f" - }, "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9626", "metadata_owner_org": "abc-org-123", "metadata_provider_org": "abc-org-123", @@ -11715,13 +13374,12 @@ "research_dataset": { "access_rights": { "access_type": { - "definition": { - "en": "A statement or formal explanation of the meaning of a concept." - }, "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", - "in_scheme": "http://uri.of.filetype.concept/scheme", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", "pref_label": { - "en": "pref label for this type" + "en": "Open", + "fi": "Avoin", + "und": "Avoin" } }, "access_url": { @@ -11745,13 +13403,21 @@ "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } } ], "restriction_grounds": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other" + "identifier": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds/code/other", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/restriction_grounds", + "pref_label": { + "en": "Restricted access due to other reasons", + "fi": "Saatavuutta rajoitettu muulla perusteella", + "sv": "Begr\u00e4nsad \u00e5tkomst av \u00f6vriga sk\u00e4l", + "und": "Saatavuutta rajoitettu muulla perusteella" + } } ] }, @@ -11761,7 +13427,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/funding_acquisition", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Funding acquisition", + "fi": "Rahoituksen hankinta", + "sv": "Anskaffande av finansiering", + "und": "Rahoituksen hankinta" + } } ], "email": "kalle.kontribuuttaaja@csc.fi", @@ -11787,7 +13460,14 @@ "@type": "Person", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ProjectLeader", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Project Leader", + "fi": "Projektin johtaja", + "sv": "Projektledare", + "und": "Projektin johtaja" + } } ], "email": "franzibald.kontribuuttaaja@csc.fi", @@ -11815,7 +13495,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/conceptualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Conceptualization", + "fi": "Tutkimuksen muotoilu", + "sv": "Formulering av forskningen", + "und": "Tutkimuksen muotoilu" + } } ], "member_of": { @@ -11834,7 +13521,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } } ], "identifier": "id:of:curator:default", @@ -11869,13 +13563,25 @@ ], "infrastructure": [ { - "identifier": "urn-nbn-fi-research-infras-2016072530" + "identifier": "urn-nbn-fi-research-infras-2016072530", + "in_scheme": "https://avaa.tdata.fi/api/jsonws/tupa-portlet.Infrastructures/get-all-infrastructures", + "pref_label": { + "en": "Accelerator laboratory of the university of Jyv\u00e4skyl\u00e4", + "fi": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio", + "und": "Jyv\u00e4skyl\u00e4n yliopiston fysiikan laitoksen Kiihdytinlaboratorio" + } } ], "is_output_of": [ { "funder_type": { - "identifier": "tekes" + "identifier": "http://uri.suomi.fi/codelist/fairdata/funder_type/code/tekes", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/funder_type", + "pref_label": { + "en": "Tekes", + "fi": "Tekes", + "und": "Tekes" + } }, "has_funder_identifier": "funderprojectidentifier", "has_funding_agency": [ @@ -11883,7 +13589,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Editor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Editor", + "fi": "Julkaisutoimittaja", + "sv": "Redakt\u00f6r", + "und": "Julkaisutoimittaja" + } } ], "email": "rahoitus@rahaorg.fi", @@ -11906,7 +13619,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/HostingInstitution", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Hosting Institution", + "fi": "S\u00e4ilytt\u00e4v\u00e4 organisaatio", + "sv": "Bevarande organisation", + "und": "S\u00e4ilytt\u00e4v\u00e4 organisaatio" + } } ], "email": "info@csc.fi", @@ -11930,7 +13650,13 @@ ], "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9626", @@ -11943,7 +13669,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/ContactPerson", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Point of contact", + "fi": "Yhteystiedot", + "sv": "Kontaktuppgifter", + "und": "Yhteystiedot" + } } ], "email": "info@csc.fi", @@ -11974,7 +13707,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCollector", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Collector", + "fi": "Aineiston ker\u00e4\u00e4j\u00e4", + "sv": "Datainsamling", + "und": "Aineiston ker\u00e4\u00e4j\u00e4" + } } ], "email": "info@csc.fi", @@ -12006,14 +13746,26 @@ "en": "Description of provenance activity" }, "event_outcome": { - "identifier": "Success" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/success", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Success", + "fi": "Onnistunut", + "sv": "Framg\u00e5ngsrik", + "und": "Onnistunut" + } }, "lifecycle_event": { "definition": { "en": "A statement or formal explanation of the meaning of a concept." }, "identifier": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event/code/collected", - "in_scheme": "http://uri.of.filetype.concept/scheme" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/lifecycle_event", + "pref_label": { + "en": "Collected", + "fi": "Ker\u00e4tty", + "und": "Ker\u00e4tty" + } }, "outcome_description": { "en": "A grand success", @@ -12024,7 +13776,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, "temporal": { @@ -12091,7 +13850,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataCurator", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Curator", + "fi": "Aineiston k\u00e4sittelij\u00e4", + "sv": "Databehandling", + "und": "Aineiston k\u00e4sittelij\u00e4" + } } ], "email": "info@csc.fi", @@ -12111,21 +13877,41 @@ "en": "Description of other provenance activity" }, "event_outcome": { - "identifier": "Failure" + "identifier": "http://uri.suomi.fi/codelist/fairdata/event_outcome/code/failure", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/event_outcome", + "pref_label": { + "en": "Failure", + "fi": "Ep\u00e4onnistunut", + "sv": "Misslyckad", + "und": "Ep\u00e4onnistunut" + } }, "outcome_description": { "en": "A grand failure", "fi": "Ep\u00e4onnistui" }, "preservation_event": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd" + "identifier": "http://uri.suomi.fi/codelist/fairdata/preservation_event/code/upd", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/preservation_event", + "pref_label": { + "en": "Object update", + "fi": "Objektin p\u00e4ivitys", + "und": "Objektin p\u00e4ivitys" + } }, "spatial": { "alt": "11.111", "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p105917" + "identifier": "http://www.yso.fi/onto/yso/p105917", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Southern Finland Province", + "fi": "Etel\u00e4-Suomi", + "sv": "S\u00f6dra Finland", + "und": "Etel\u00e4-Suomi" + } } }, "temporal": { @@ -12141,7 +13927,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/Distributor", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Distributor", + "fi": "Jakelija", + "sv": "Distribut\u00f6r", + "und": "Jakelija" + } } ], "email": "info@csc.fi", @@ -12192,11 +13985,22 @@ "fi": "Ulkopuolinen aineisto 1" }, "type": { - "identifier": "physical_object" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/physical_object", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Physical object", + "fi": "Fyysinen objekti", + "und": "Fyysinen objekti" + } } }, "relation_type": { - "identifier": "wasDerivedFrom" + "identifier": "http://www.w3.org/ns/prov#wasDerivedFrom", + "pref_label": { + "en": "Was derived from", + "fi": "Johdettu aineistosta", + "und": "Johdettu aineistosta" + } } }, { @@ -12210,11 +14014,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "collection" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/collection", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Collection", + "fi": "Kokoelma", + "und": "Kokoelma" + } } }, "relation_type": { - "identifier": "cites" + "identifier": "http://purl.org/spar/cito/cites", + "pref_label": { + "en": "Cites", + "fi": "Viittaa", + "und": "Viittaa" + } } }, { @@ -12228,11 +14043,22 @@ "fi": "Ulkopuolinen aineisto 2" }, "type": { - "identifier": "service" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/service", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "Service", + "fi": "Palvelu", + "und": "Palvelu" + } } }, "relation_type": { - "identifier": "isPartOf" + "identifier": "http://purl.org/dc/terms/isPartOf", + "pref_label": { + "en": "Is part of", + "fi": "Kuuluu aineistoon", + "und": "Kuuluu aineistoon" + } } }, { @@ -12246,11 +14072,22 @@ "fi": "Ulkopuolinen asia 2" }, "type": { - "identifier": "ui" + "identifier": "http://uri.suomi.fi/codelist/fairdata/resource_type/code/ui", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/resource_type", + "pref_label": { + "en": "User interface", + "fi": "K\u00e4ytt\u00f6liittym\u00e4", + "und": "K\u00e4ytt\u00f6liittym\u00e4" + } } }, "relation_type": { - "identifier": "hasPreviousVersion" + "identifier": "http://www.w3.org/ns/adms#previous", + "pref_label": { + "en": "Has previous version", + "fi": "Edellinen versio", + "und": "Edellinen versio" + } } } ], @@ -12291,9 +14128,10 @@ "en": "Free account of the rights" }, "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", - "license": "https://url.of.license.which.applies.org", + "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } } ], @@ -12327,9 +14165,10 @@ "en": "Free account of the rights" }, "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/Apache-2.0", - "license": "https://url.of.license.which.applies.org", + "license": "https://url.of.license.which.applies.here.org", "title": { - "en": "A name given to the resource" + "en": "Apache Software License 2.0", + "und": "Apache Software License 2.0" } } ], @@ -12353,7 +14192,14 @@ "@type": "Organization", "contributor_type": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_type/code/DataManager", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_type", + "pref_label": { + "en": "Data Manager", + "fi": "Aineiston hallinnoija", + "sv": "Dataadministration", + "und": "Aineiston hallinnoija" + } } ], "email": "info@csc.fi", @@ -12384,7 +14230,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } }, { @@ -12392,7 +14245,14 @@ "full_address": "The complete address written as a string, with or without formatting", "geographic_name": "Geographic name 2", "place_uri": { - "identifier": "http://www.yso.fi/onto/yso/p107966" + "identifier": "http://www.yso.fi/onto/yso/p107966", + "in_scheme": "http://www.yso.fi/onto/yso/places", + "pref_label": { + "en": "Olostunturi", + "fi": "Olostunturi", + "sv": "Olostunturi", + "und": "Olostunturi" + } } } ], @@ -12444,12 +14304,6 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", - "editor": { - "creator_id": "055ea4dade5ab2145954f56d4b51cef0", - "identifier": "qvain", - "owner_id": "055ea4dade5ab2145954f56d4b51cef0", - "record_id": "955e904-e3dd-4d7e-99f1-3fed446f9027" - }, "files": [ 19, 20 @@ -12464,7 +14318,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -12472,7 +14332,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -12519,7 +14386,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9610-alt-1", @@ -12570,7 +14443,13 @@ "research_dataset": { "access_rights": { "access_type": { - "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open" + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/access_type", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } } }, "creator": [ @@ -12578,7 +14457,14 @@ "@type": "Person", "contributor_role": [ { - "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization" + "identifier": "http://uri.suomi.fi/codelist/fairdata/contributor_role/code/visualization", + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/contributor_role", + "pref_label": { + "en": "Visualization", + "fi": "Visualisointi", + "sv": "Visualisering", + "und": "Visualisointi" + } } ], "member_of": { @@ -12625,7 +14511,13 @@ "issued": "2019-01-01", "language": [ { - "identifier": "http://lexvo.org/id/iso639-3/eng" + "identifier": "http://lexvo.org/id/iso639-3/eng", + "title": { + "en": "English", + "fi": "englanti", + "sv": "engelska", + "und": "englanti" + } } ], "metadata_version_identifier": "1955e904-e3dd-4d7e-99f1-3fed446f9610-alt-2", diff --git a/src/metax_api/tests/utils.py b/src/metax_api/tests/utils.py index 9e167160..6fd25b6a 100755 --- a/src/metax_api/tests/utils.py +++ b/src/metax_api/tests/utils.py @@ -28,10 +28,7 @@ def assert_catalog_record_is_open_access(cr): from metax_api.models.catalog_record import ACCESS_TYPES access_type = ( - cr["research_dataset"] - .get("access_rights", {}) - .get("access_type", {}) - .get("identifier", "") + cr["research_dataset"].get("access_rights", {}).get("access_type", {}).get("identifier", "") ) assert access_type == ACCESS_TYPES["open"] @@ -40,10 +37,7 @@ def assert_catalog_record_not_open_access(cr): from metax_api.models.catalog_record import ACCESS_TYPES access_type = ( - cr["research_dataset"] - .get("access_rights", {}) - .get("access_type", {}) - .get("identifier", "") + cr["research_dataset"].get("access_rights", {}).get("access_type", {}).get("identifier", "") ) assert access_type != ACCESS_TYPES["open"] @@ -135,7 +129,7 @@ def create_end_user_data_catalogs(self): date_created=get_tz_aware_now_without_micros(), catalog_record_services_create="testuser,api_auth_user,metax", catalog_record_services_edit="testuser,api_auth_user,metax", - catalog_record_services_read='testuser,api_auth_user,metax' + catalog_record_services_read="testuser,api_auth_user,metax", ) def create_legacy_data_catalogs(self): @@ -153,7 +147,7 @@ def create_legacy_data_catalogs(self): date_created=get_tz_aware_now_without_micros(), catalog_record_services_create="testuser,api_auth_user,metax", catalog_record_services_edit="testuser,api_auth_user,metax", - catalog_record_services_read='testuser,api_auth_user,metax' + catalog_record_services_read="testuser,api_auth_user,metax", ) def _set_http_authorization(self, credentials_type): @@ -162,9 +156,7 @@ def _set_http_authorization(self, credentials_type): self.client.credentials() elif credentials_type == "service": - self._use_http_authorization( - username="metax" - ) + self._use_http_authorization(username="metax") elif credentials_type == "owner": self._use_http_authorization(method="bearer", token=self.token) self._mock_token_validation_succeeds() @@ -207,18 +199,14 @@ def _use_http_authorization( password = user["password"] else: if not password: - raise Exception( - "Missing parameter 'password' for HTTP Authorization header" - ) + raise Exception("Missing parameter 'password' for HTTP Authorization header") header_value = "Basic %s" % b64encode( bytes("%s:%s" % (username, password), "utf-8") ).decode("utf-8") elif method == "bearer": - assert ( - token is not None - ), "token (dictionary) is required when using auth method bearer" + assert token is not None, "token (dictionary) is required when using auth method bearer" header_value = "Bearer %s" % generate_test_token(token) self.client.credentials(HTTP_AUTHORIZATION=header_value) @@ -269,22 +257,16 @@ def _get_object_from_test_data(self, model_name, requested_index=0): def _create_cr_for_owner(self, pk_for_template_cr, data): self.token = get_test_oidc_token() - if "editor" in data: - data.pop("editor", None) data["user_created"] = self.token["CSCUserName"] data["metadata_provider_user"] = self.token["CSCUserName"] data["metadata_provider_org"] = self.token["schacHomeOrganization"] data["metadata_owner_org"] = self.token["schacHomeOrganization"] - data["data_catalog"][ - "identifier" - ] = django_settings.END_USER_ALLOWED_DATA_CATALOGS[0] + data["data_catalog"]["identifier"] = django_settings.END_USER_ALLOWED_DATA_CATALOGS[0] data.pop("identifier", None) data["research_dataset"].pop("preferred_identifier", None) - response = self.client.post( - f"/rest/{self.api_version}/datasets", data, format="json" - ) + response = self.client.post(f"/rest/{self.api_version}/datasets", data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) return response.data["id"] @@ -295,9 +277,7 @@ def get_open_cr_with_files_and_dirs_from_api_with_file_details( from metax_api.models.catalog_record import ACCESS_TYPES # Use http auth to get complete details of the catalog record - self._use_http_authorization( - username="metax" - ) + self._use_http_authorization(username="metax") pk = 13 if set_owner: @@ -306,9 +286,7 @@ def get_open_cr_with_files_and_dirs_from_api_with_file_details( ) pk = self._create_cr_for_owner(pk, response.data) - CatalogRecord.objects.get( - pk=pk - ).calculate_directory_byte_sizes_and_file_counts() + CatalogRecord.objects.get(pk=pk).calculate_directory_byte_sizes_and_file_counts() if use_login_access_type: response_data = self.client.get( @@ -344,28 +322,22 @@ def get_open_cr_with_files_and_dirs_from_api_with_file_details( return response.data - def get_restricted_cr_with_files_and_dirs_from_api_with_file_details( - self, set_owner=False - ): + def get_restricted_cr_with_files_and_dirs_from_api_with_file_details(self, set_owner=False): from metax_api.models import CatalogRecord from metax_api.models.catalog_record import ACCESS_TYPES # Use http auth to get complete details of the catalog record - self._use_http_authorization( - "metax" - ) + self._use_http_authorization("metax") pk = 13 - response = self.client.get( - f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata" - ) + response = self.client.get(f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) data = response.data # Set access_type to restricted - data["research_dataset"]["access_rights"]["access_type"][ - "identifier" - ] = ACCESS_TYPES["restricted"] + data["research_dataset"]["access_rights"]["access_type"]["identifier"] = ACCESS_TYPES[ + "restricted" + ] if set_owner: pk = self._create_cr_for_owner(pk, data) @@ -377,9 +349,7 @@ def get_restricted_cr_with_files_and_dirs_from_api_with_file_details( ) self.assertEqual(response.status_code, status.HTTP_200_OK) - CatalogRecord.objects.get( - pk=pk - ).calculate_directory_byte_sizes_and_file_counts() + CatalogRecord.objects.get(pk=pk).calculate_directory_byte_sizes_and_file_counts() response = self.client.get( f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata&file_details" ) @@ -397,42 +367,32 @@ def get_restricted_cr_with_files_and_dirs_from_api_with_file_details( return response.data - def get_embargoed_cr_with_files_and_dirs_from_api_with_file_details( - self, is_available - ): + def get_embargoed_cr_with_files_and_dirs_from_api_with_file_details(self, is_available): from metax_api.models import CatalogRecord from metax_api.models.catalog_record import ACCESS_TYPES # Use http auth to get complete details of the catalog record - self._use_http_authorization( - "metax" - ) + self._use_http_authorization("metax") pk = 13 - response = self.client.get( - f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata" - ) + response = self.client.get(f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata") self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) data = response.data # Set access_type to embargo - data["research_dataset"]["access_rights"]["access_type"][ - "identifier" - ] = ACCESS_TYPES["embargo"] + data["research_dataset"]["access_rights"]["access_type"]["identifier"] = ACCESS_TYPES[ + "embargo" + ] if is_available: data["research_dataset"]["access_rights"]["available"] = "2000-01-01" else: data["research_dataset"]["access_rights"]["available"] = "3000-01-01" - response = self.client.put( - f"/rest/{self.api_version}/datasets/13", data, format="json" - ) + response = self.client.put(f"/rest/{self.api_version}/datasets/13", data, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - CatalogRecord.objects.get( - pk=pk - ).calculate_directory_byte_sizes_and_file_counts() + CatalogRecord.objects.get(pk=pk).calculate_directory_byte_sizes_and_file_counts() response = self.client.get( f"/rest/{self.api_version}/datasets/{pk}?include_user_metadata&file_details" ) @@ -475,9 +435,7 @@ def _get_new_file_data( if not project: project = "research_project_112" if not directory_path: - directory_path = ( - "/prj_112_root/science_data_C/phase_2/2017/10/dir_" + file_n - ) + directory_path = "/prj_112_root/science_data_C/phase_2/2017/10/dir_" + file_n if not file_path: file_path = directory_path + "/file_" + file_n diff --git a/src/metax_api/urls.py b/src/metax_api/urls.py index 8f624a44..85cdf746 100755 --- a/src/metax_api/urls.py +++ b/src/metax_api/urls.py @@ -31,33 +31,35 @@ from metax_api.api.rpc.base.router import api_urlpatterns as rpc_api_v1 from metax_api.api.rpc.v2.router import api_urlpatterns as rpc_api_v2 from metax_api.views.router import view_urlpatterns +from metax_api.views.test_view import TestView v1_urls = [ - url('', include(view_urlpatterns)), - url(r'^oai/', oaipmh, name='oai'), - url(r'^rest/', include(rest_api_v1)), - url(r'^rest/v1/', include(rest_api_v1)), - url(r'^rpc/', include(rpc_api_v1)), - url(r'^rpc/v1/', include(rpc_api_v1)), + url("", include(view_urlpatterns)), + url(r"^oai/", oaipmh, name="oai"), + url(r"^rest/", include(rest_api_v1)), + url(r"^rest/v1/", include(rest_api_v1)), + url(r"^rpc/", include(rpc_api_v1)), + url(r"^rpc/v1/", include(rpc_api_v1)), ] v2_urls = [ - url(r'^rest/v2/', include(rest_api_v2)), - url(r'^rpc/v2/', include(rpc_api_v2)), + url(r"^rest/v2/", include(rest_api_v2)), + url(r"^rpc/v2/", include(rpc_api_v2)), ] urlpatterns = [] -if 'v1' in django_settings.API_VERSIONS_ENABLED: +if "v1" in django_settings.API_VERSIONS_ENABLED: urlpatterns += v1_urls -if 'v2' in django_settings.API_VERSIONS_ENABLED: +if "v2" in django_settings.API_VERSIONS_ENABLED: urlpatterns += v2_urls if django_settings.WATCHMAN_CONFIGURED: - urlpatterns += [re_path(r'^watchman/', include('watchman.urls'))] + urlpatterns += [re_path(r"^watchman/", include("watchman.urls"))] if django_settings.DEBUG: urlpatterns += [ - path('__debug__/', include(debug_toolbar.urls)), + path("test_view/", TestView.as_view(), name="test_view"), + path("__debug__/", include(debug_toolbar.urls)), ] diff --git a/src/metax_api/utils/convert_yaml_to_html.py b/src/metax_api/utils/convert_yaml_to_html.py new file mode 100644 index 00000000..cb3c919a --- /dev/null +++ b/src/metax_api/utils/convert_yaml_to_html.py @@ -0,0 +1,119 @@ +import logging +import json +import yaml +from os import path, makedirs +from django.conf import settings + +_logger = logging.getLogger(__name__) + +TEMPLATE = """ + + + + + Swagger UI + + + + + + + +
+ + + + + + + +""" + +FAIL_TEMPLATE = """ + + + + + Swagger file creation failed + + +

Swagger file creation failed!

+ + +""" + + +def yaml_to_html_convert(): + + try: + for api_version in ["v1", "v2"]: + inpath = path.join(settings.SWAGGER_YAML_PATH, api_version, 'swagger.yaml') + outpath = path.join(settings.SWAGGER_HTML_PATH, api_version, 'swagger.html') + + if not path.exists(path.dirname(outpath)): + try: + makedirs(path.dirname(outpath)) + except OSError as exc: + _logger.error(exc) + raise + + with open(outpath, 'w') as outfile: + try: + with open(inpath, 'r') as infile: + readdata = infile.read() + indata = readdata.replace("__METAX_ENV_DOMAIN__", settings.SERVER_DOMAIN_NAME) + spec = yaml.load(indata, Loader=yaml.FullLoader) + outfile.write(TEMPLATE % json.dumps(spec)) + + except FileNotFoundError: + outfile.write(FAIL_TEMPLATE) + except yaml.YAMLError as exc: + _logger.error("YAML loading failed") + _logger.error(exc) + outfile.write(FAIL_TEMPLATE) + except json.decoder.JSONDecodeError as exc: + _logger.error("JSON loading failed") + _logger.error(exc) + + except PermissionError: + _logger.error("Permission error") diff --git a/src/metax_api/utils/reference_data_loader.py b/src/metax_api/utils/reference_data_loader.py index 392d98b7..26df7679 100755 --- a/src/metax_api/utils/reference_data_loader.py +++ b/src/metax_api/utils/reference_data_loader.py @@ -14,7 +14,7 @@ _logger = logging.getLogger(__name__) -class ReferenceDataLoader(): +class ReferenceDataLoader: REF_DATA_LOAD_NUM = 0 @@ -31,43 +31,54 @@ def populate_cache_reference_data(cls, cache, settings=django_settings): cache: cache object to use for saving settings: override elasticsearch settings in settings.py """ - if not cache.get_or_set('reference_data_load_executing', True, ex=120): - return 'reload_started_by_other' + if not cache.get_or_set("reference_data_load_executing", True, ex=120): + return "reload_started_by_other" - _logger.info('ReferenceDataLoader - populating cache...') + _logger.info("ReferenceDataLoader - populating cache...") if executing_test_case(): - _logger.info('(Note: populating test suite cache)') + _logger.info("(Note: populating test suite cache)") try: reference_data = cls._fetch_reference_data(settings) except: - _logger.exception('Reference data fetch failed') + _logger.exception("Reference data fetch failed") raise - cache.set('reference_data', reference_data) + cache.set("reference_data", reference_data) errors = None - reference_data_check = cache.get('reference_data', master=True) - if 'reference_data' not in reference_data_check.keys(): - _logger.warning('Key reference_data missing from reference data - ' - 'something went wrong during cache population?') + reference_data_check = cache.get("reference_data", master=True) + if "reference_data" not in reference_data_check.keys(): + _logger.warning( + "Key reference_data missing from reference data - " + "something went wrong during cache population?" + ) errors = True raise Exception("reference data loading failed") - if 'organization_data' not in reference_data_check.keys(): - _logger.warning('Key organization_data missing from reference data - ' - 'something went wrong during cache population?') + if "organization_data" not in reference_data_check.keys(): + _logger.warning( + "Key organization_data missing from reference data - " + "something went wrong during cache population?" + ) errors = True if not errors: if not isinstance(settings, dict): settings = settings.ELASTICSEARCH - cache.set('ref_data_up_to_date', True, ex=django_settings.REFERENCE_DATA_RELOAD_INTERVAL) + cache.set( + "ref_data_up_to_date", + True, + ex=django_settings.REFERENCE_DATA_RELOAD_INTERVAL, + ) - _logger.info('ReferenceDataLoader - %s' % ('failed to populate cache' if errors else 'cache populated')) - cache.delete('reference_data_load_executing') + _logger.info( + "ReferenceDataLoader - %s" + % ("failed to populate cache" if errors else "cache populated") + ) + cache.delete("reference_data_load_executing") @classmethod def _fetch_reference_data(cls, settings): @@ -77,7 +88,7 @@ def _fetch_reference_data(cls, settings): settings = settings.ELASTICSEARCH connection_params = cls.get_connection_parameters(settings) - esclient, scan = cls.get_es_imports(settings['HOSTS'], connection_params) + esclient, scan = cls.get_es_imports(settings["HOSTS"], connection_params) reference_data = {} for index_name in esclient.indices.get_mapping().keys(): @@ -87,21 +98,21 @@ def _fetch_reference_data(cls, settings): # if local elasticsearch is not used. aggr_types = esclient.search( index=index_name, - body={"aggs": { "types": {"terms": {"field": "type", "size": 30}}}}, - filter_path='aggregations', - _source='type', - scroll='1m' + body={"aggs": {"types": {"terms": {"field": "type", "size": 30}}}}, + filter_path="aggregations", + _source="type", + scroll="1m", ) - for type_name in [ b['key'] for b in aggr_types['aggregations']['types']['buckets'] ]: + for type_name in [b["key"] for b in aggr_types["aggregations"]["types"]["buckets"]]: reference_data[index_name][type_name] = [] # must use wildcard query here because organization_data does not have their 'type' # field indexed in the old 5.8 version.. This is fixed in the new ES cluster so this could # be changed after all envs is using the new version. all_rows = scan( esclient, - query={'query': {'wildcard': {'id': {'value': f'{type_name}*'}}}}, - index=index_name + query={"query": {"wildcard": {"id": {"value": f"{type_name}*"}}}}, + index=index_name, ) for row in all_rows: @@ -111,44 +122,58 @@ def _fetch_reference_data(cls, settings): try: # should always be present - entry = { 'uri': row['_source']['uri'] } + entry = {"uri": row["_source"]["uri"]} except KeyError: - _logger.warning('Elasticsearch document missing uri in index {0} type {1}: {2}'.format( - index_name, type_name, row)) + _logger.warning( + "Elasticsearch document missing uri in index {0} type {1}: {2}".format( + index_name, type_name, row + ) + ) continue try: # should always be present - entry['code'] = row['_source']['code'] + entry["code"] = row["_source"]["code"] except KeyError: # error, but not blocking. place key 'code' anyway so that the existence # of the key does not have to be checked elsewhere - entry['code'] = None - _logger.warning('Elasticsearch document missing code in index {0} type {1}: {2}'.format( - index_name, type_name, row)) + entry["code"] = None + _logger.warning( + "Elasticsearch document missing code in index {0} type {1}: {2}".format( + index_name, type_name, row + ) + ) - label = row['_source'].get('label', None) - scheme = row['_source'].get('scheme', None) + label = row["_source"].get("label", None) + scheme = row["_source"].get("scheme", None) # dont want empty dicts loitering in the cache if label: - entry['label'] = label + entry["label"] = label if scheme: - entry['scheme'] = scheme + entry["scheme"] = scheme - if type_name == 'location': - entry['wkt'] = row['_source'].get('wkt', None) + if type_name == "location": + entry["wkt"] = row["_source"].get("wkt", None) - if type_name == 'license' and 'same_as' in row['_source'] and len(row['_source']['same_as']) > 0: - entry['same_as'] = row['_source']['same_as'][0] + if ( + type_name == "license" + and "same_as" in row["_source"] + and len(row["_source"]["same_as"]) > 0 + ): + entry["same_as"] = row["_source"]["same_as"][0] - if type_name == 'file_format_version': - entry['input_file_format'] = row['_source'].get('input_file_format', None) - entry['output_format_version'] = row['_source'].get('output_format_version', None) + if type_name == "file_format_version": + entry["input_file_format"] = row["_source"].get("input_file_format", None) + entry["output_format_version"] = row["_source"].get( + "output_format_version", None + ) - if type_name == 'organization' and row['_source'].get('parent_id', False): - entry['parent_org_code'] = row['_source']['parent_id'][len('organization_'):] + if type_name == "organization" and row["_source"].get("parent_id", False): + entry["parent_org_code"] = row["_source"]["parent_id"][ + len("organization_") : + ] reference_data[index_name][type_name].append(entry) @@ -159,12 +184,18 @@ def get_connection_parameters(settings): """ https://docs.objectrocket.com/elastic_python_examples.html """ - if settings['HOSTS'][0] != 'localhost': - conf = { 'send_get_body_as': 'GET' } - if settings.get('USE_SSL', False): - conf.update({ 'port': 443, 'use_ssl': True, 'verify_certs': True, }) - if settings.get('PORT', False): - conf.update({ 'port': settings['PORT'] }) + if settings["HOSTS"][0] != "localhost": + conf = {"send_get_body_as": "GET"} + if settings.get("USE_SSL", False): + conf.update( + { + "port": 443, + "use_ssl": True, + "verify_certs": True, + } + ) + if settings.get("PORT", False): + conf.update({"port": settings["PORT"]}) return conf _logger.warning("returning empty connection parameters") return {} diff --git a/src/metax_api/utils/utils.py b/src/metax_api/utils/utils.py index 759427ca..4871d077 100755 --- a/src/metax_api/utils/utils.py +++ b/src/metax_api/utils/utils.py @@ -17,11 +17,11 @@ class IdentifierType(Enum): - URN = 'urn' - DOI = 'doi' + URN = "urn" + DOI = "doi" -class DelayedLog(): +class DelayedLog: """ A callable that can be passed to CallableService as a post_request_callable, @@ -40,16 +40,16 @@ def executing_test_case(): """ Returns True whenever code is being executed by automatic test cases """ - return 'test' in sys.argv + return "test" in sys.argv def datetime_to_str(date_obj): if isinstance(date_obj, datetime): - return date_obj.strftime('%Y-%m-%dT%H:%M:%SZ') + return date_obj.strftime("%Y-%m-%dT%H:%M:%SZ") elif datetime is None: return None else: - assert isinstance(date_obj, datetime), 'date_obj must be datetime object or None' + assert isinstance(date_obj, datetime), "date_obj must be datetime object or None" def parse_timestamp_string_to_tz_aware_datetime(timestamp_str): @@ -82,7 +82,7 @@ def get_tz_aware_now_without_micros(): def generate_uuid_identifier(urn_prefix=False): if urn_prefix: - return 'urn:nbn:fi:att:%s' % str(uuid4()) + return "urn:nbn:fi:att:%s" % str(uuid4()) return str(uuid4()) @@ -93,10 +93,15 @@ def is_metax_generated_doi_identifier(identifier): :param identifier: :return: boolean """ - if not identifier or not hasattr(settings, 'DATACITE') or not settings.DATACITE.get('PREFIX', False): + if ( + not identifier + or not hasattr(settings, "DATACITE") + or not settings.DATACITE.get("PREFIX", False) + ): return False - return identifier.startswith('doi:{0}/'.format(settings.DATACITE.get('PREFIX'))) + return identifier.startswith("doi:{0}/".format(settings.DATACITE.get("PREFIX"))) + def is_remote_doi_identifier(identifier): """ @@ -105,12 +110,15 @@ def is_remote_doi_identifier(identifier): :param identifier: :return: boolean """ - if not identifier or not settings.DATACITE.get('PREFIX', False): + if not identifier or not settings.DATACITE.get("PREFIX", False): return False - if identifier.startswith('doi:') and not identifier.startswith('doi:{0}/'.format(settings.DATACITE.get('PREFIX'))): + if identifier.startswith("doi:") and not identifier.startswith( + "doi:{0}/".format(settings.DATACITE.get("PREFIX")) + ): return True + def is_metax_generated_urn_identifier(identifier): """ Check whether given identifier is a metax generated urn identifier @@ -121,7 +129,7 @@ def is_metax_generated_urn_identifier(identifier): if not identifier: return False - return identifier.startswith('urn:nbn:fi:att:') or identifier.startswith('urn:nbn:fi:csc') + return identifier.startswith("urn:nbn:fi:att:") or identifier.startswith("urn:nbn:fi:csc") def generate_doi_identifier(doi_suffix=None): @@ -135,13 +143,13 @@ def generate_doi_identifier(doi_suffix=None): doi_suffix = generate_uuid_identifier() doi_prefix = None - if hasattr(settings, 'DATACITE'): - doi_prefix = settings.DATACITE.get('PREFIX', None) + if hasattr(settings, "DATACITE"): + doi_prefix = settings.DATACITE.get("PREFIX", None) if not doi_prefix: raise Exception("PREFIX must be defined in settings DATACITE dictionary") if not doi_suffix: raise ValueError("DOI suffix must be provided in order to create a DOI identifier") - return 'doi:{0}/{1}'.format(doi_prefix, doi_suffix) + return "doi:{0}/{1}".format(doi_prefix, doi_suffix) def extract_doi_from_doi_identifier(doi_identifier): @@ -152,16 +160,16 @@ def extract_doi_from_doi_identifier(doi_identifier): :param doi_identifier: Must start with doi:10. for this method to work properly :return: If the doi_identifier does not start with doi:10., return None. Otherwise return doi starting from 10. """ - if doi_identifier and doi_identifier.startswith('doi:10.'): - return doi_identifier[doi_identifier.index('10.'):] + if doi_identifier and doi_identifier.startswith("doi:10."): + return doi_identifier[doi_identifier.index("10.") :] return None def get_identifier_type(identifier): if identifier: - if identifier.startswith('doi:'): + if identifier.startswith("doi:"): return IdentifierType.DOI - elif identifier.startswith('urn:'): + elif identifier.startswith("urn:"): return IdentifierType.URN return None @@ -176,11 +184,16 @@ def remove_keys_recursively(obj, fields_to_remove): """ if isinstance(obj, dict): obj = { - key: remove_keys_recursively(value, fields_to_remove) for key, value in obj.items() + key: remove_keys_recursively(value, fields_to_remove) + for key, value in obj.items() if key not in fields_to_remove } elif isinstance(obj, list): - obj = [remove_keys_recursively(item, fields_to_remove) for item in obj if item not in fields_to_remove] + obj = [ + remove_keys_recursively(item, fields_to_remove) + for item in obj + if item not in fields_to_remove + ] return obj @@ -200,8 +213,8 @@ def leave_keys_in_dict(dict_obj, fields_to_leave): if executing_test_case(): - class TestJsonLogger(): + class TestJsonLogger: def info(self, *args, **kwargs): pass @@ -216,4 +229,4 @@ def debug(self, *args, **kwargs): json_logger = TestJsonLogger() else: - json_logger = structlog.get_logger('structlog') + json_logger = structlog.get_logger("structlog") diff --git a/src/metax_api/views/router.py b/src/metax_api/views/router.py index 02a44b82..deaaa082 100755 --- a/src/metax_api/views/router.py +++ b/src/metax_api/views/router.py @@ -6,10 +6,13 @@ # :license: MIT from django.conf.urls import url +from django.views.generic import TemplateView from metax_api.views.secure import secure_view view_urlpatterns = [ - url(r'^logout?', secure_view.SecureLogoutView.as_view()), - url(r'^secure/login?', secure_view.SecureLoginView.as_view()), + url(r"^logout?", secure_view.SecureLogoutView.as_view()), + url(r"^secure/login?", secure_view.SecureLoginView.as_view()), + url(r"^swagger/v1", TemplateView.as_view(template_name='swagger/v1/swagger.html')), + url(r"^swagger/v2", TemplateView.as_view(template_name='swagger/v2/swagger.html')), ] diff --git a/src/metax_api/views/secure/secure_view.py b/src/metax_api/views/secure/secure_view.py index ed9fd4ad..2c4cbba5 100755 --- a/src/metax_api/views/secure/secure_view.py +++ b/src/metax_api/views/secure/secure_view.py @@ -19,47 +19,49 @@ class SecureLoginView(TemplateView): - def get(self, request, **kwargs): """ Reached through a redirect after a successful OIDC authentication. Parse the received id_token, and show selected contents of it to the user on a web page. """ - _logger.debug('extracting information from token') + _logger.debug("extracting information from token") - token_payload = json.loads(request.META['HTTP_OIDC_ID_TOKEN_PAYLOAD']) + token_payload = json.loads(request.META["HTTP_OIDC_ID_TOKEN_PAYLOAD"]) _logger.debug(token_payload) try: json_logger.info( - event='user_login_visit', - user_id=token_payload.get('CSCUserName', token_payload['eppn']), - org_id=token_payload.get('schacHomeOrganization', 'org_missing'), + event="user_login_visit", + user_id=token_payload.get("CSCUserName", token_payload["eppn"]), + org_id=token_payload.get("schacHomeOrganization", "org_missing"), ) except KeyError: - _logger.error('token_payload has no CSCUserName or eppn') + _logger.error("token_payload has no CSCUserName or eppn") - idm_account_exists = len(token_payload.get('CSCUserName', '')) > 0 + idm_account_exists = len(token_payload.get("CSCUserName", "")) > 0 - home_org_exists = len(token_payload.get('schacHomeOrganization', '')) > 0 + home_org_exists = len(token_payload.get("schacHomeOrganization", "")) > 0 context = { - 'email': token_payload['email'], - 'idm_account_exists': idm_account_exists, - 'home_org_exists': home_org_exists, - 'token_string': request.META['HTTP_OIDC_ID_TOKEN'] if idm_account_exists and home_org_exists else '', - 'token_valid_until': datetime.fromtimestamp(token_payload['exp']).strftime('%Y-%m-%d %H:%M:%S'), - 'haka_exists': 'eppn' in token_payload, - 'logout_redirect_domain': django_settings.SERVER_DOMAIN_NAME, + "email": token_payload["email"], + "idm_account_exists": idm_account_exists, + "home_org_exists": home_org_exists, + "token_string": request.META["HTTP_OIDC_ID_TOKEN"] + if idm_account_exists and home_org_exists + else "", + "token_valid_until": datetime.fromtimestamp(token_payload["exp"]).strftime( + "%Y-%m-%d %H:%M:%S" + ), + "haka_exists": "eppn" in token_payload, + "logout_redirect_domain": django_settings.SERVER_DOMAIN_NAME, } # note: django automatically searches templates from root directory templates/ - return render(request, 'secure/auth_success.html', context=context) + return render(request, "secure/auth_success.html", context=context) class SecureLogoutView(TemplateView): - def get(self, request, **kwargs): """ After local oidc logout, redirect to OP for OP's logout procedures. diff --git a/src/metax_api/views/test_view.py b/src/metax_api/views/test_view.py new file mode 100644 index 00000000..b5303d4f --- /dev/null +++ b/src/metax_api/views/test_view.py @@ -0,0 +1,15 @@ +from django.views.generic import TemplateView + +from metax_api.models import CatalogRecord + + +class TestView(TemplateView): + template_name = "test_view.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) # .filter(cast__in=self.request.user) + + cr = CatalogRecord.objects.first() + + context["cr"] = cr + return context \ No newline at end of file From a6d3b714585880517ae82b181d90347ffcbf79ae Mon Sep 17 00:00:00 2001 From: Toni Date: Fri, 29 Oct 2021 15:07:44 +0300 Subject: [PATCH 11/21] 21/11/1 Production update * Reference data fixes --- .../migrations/0040_auto_20211006_1116.py | 72 +++++++++++++++++++ .../local-refdata/relation_type.json | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/metax_api/migrations/0040_auto_20211006_1116.py diff --git a/src/metax_api/migrations/0040_auto_20211006_1116.py b/src/metax_api/migrations/0040_auto_20211006_1116.py new file mode 100644 index 00000000..1256fb18 --- /dev/null +++ b/src/metax_api/migrations/0040_auto_20211006_1116.py @@ -0,0 +1,72 @@ +# Generated by Django 3.1.12 on 2021-10-06 08:16 + +from django.db import migrations + +import logging + +logger = logging.getLogger(__name__) + +def replace_relation_type_labels(cr, language, old_label, new_label): + logger.info(f"replacing relation type label ({language}): {old_label} with {new_label}") + if cr.research_dataset.get("relation"): + for item in cr.research_dataset["relation"]: + if item.get("relation_type"): + if item.get("relation_type").get("pref_label"): + if item.get("relation_type").get("pref_label").get(language): + if item["relation_type"]["pref_label"][language] == old_label: + item["relation_type"]["pref_label"][language] = new_label + logger.info("relation type label changed") + +datasets = [ + "c1974353-a4f6-40c3-9c1c-a7ec99e28ac8", + "7c12b39b-0bd4-4611-a764-85f328450e2b", + "ddd9854f-eb6a-4a8b-8f0d-2d2b9c405436", + "ef224b4b-6f0b-4776-ba33-6f8bd31855cc", + "0cfa3182-48a8-4483-8ebd-2636509b8363", + "0b6cfb0e-ea71-414f-abd3-88bb415b85fe", + "fe5e67ba-fc36-449d-b83e-e1e286cc9dda", + "254acb9a-32c7-45cd-9ac1-e52ef2d6174e", + "69b0360e-2ac8-4008-ac8d-c47b8c76d47e", + "087f9d18-5196-4e6e-83a0-dfe2d94c5aa6", + "a3aa061f-1263-4fb5-887b-a465f4c3a912", + "acf70d04-31f3-4e13-a422-666e39524b71", + "5cf11735-8422-4113-beb0-997c11b7c797", + "a5a04b82-6270-4e44-b357-a1344180bc1f" +] + +def change_relation_type_labels(apps, schema_editor): + CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') + for cr in CatalogRecord.objects.filter(identifier__in=datasets): + try: + logger.info(f"changing relation type labels for cr {cr.identifier}") + replace_relation_type_labels(cr, "fi", "Liittyvä aineisto", "Liittyy") + replace_relation_type_labels(cr, "und", "Liittyvä aineisto", "Liittyy") + replace_relation_type_labels(cr, "en", "Related dataset", "Relation") + cr.save() + logger.info("cr save successful") + except Exception as e: + logger.error(e) + +def revert(apps, schema_editor): + CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') + for cr in CatalogRecord.objects.filter(identifier__in=datasets): + try: + logger.info(f"changing relation type labels for cr {cr.identifier}") + replace_relation_type_labels(cr, "fi", "Liittyy", "Liittyvä aineisto") + replace_relation_type_labels(cr, "und", "Liittyy", "Liittyvä aineisto") + replace_relation_type_labels(cr, "en", "Relation", "Related dataset") + cr.save() + logger.info("cr save successful") + except Exception as e: + logger.error(e) + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0039_auto_20210908_1151'), + ] + + operations = [ + migrations.RunPython(change_relation_type_labels, revert), + ] \ No newline at end of file diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/relation_type.json b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/relation_type.json index 66401c9e..a57a2f22 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/relation_type.json +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/local-refdata/relation_type.json @@ -1,4 +1,4 @@ -[{"id":"relation", "uri":"http://purl.org/dc/terms/relation", "label":{"fi":"Liittyvä aineisto", "en":"Related dataset"}}, +[{"id":"relation", "uri":"http://purl.org/dc/terms/relation", "label":{"fi":"Liittyy", "en":"Relation"}}, {"id":"cites", "uri":"http://purl.org/spar/cito/cites", "label":{"fi":"Viittaa", "en":"Cites"}}, {"id":"isCitedBy", "uri":"http://purl.org/spar/cito/isCitedBy", "label":{"fi":"Viitattu", "en":"Is cited by"}}, {"id":"isSupplementTo", "uri":"http://purl.org/vocab/frbr/core#isSupplementTo", "label":{"fi":"Viittaus tausta-aineistoon", "en":"Is supplement to"}}, From 54c936f38e690972b079706a9dd1007bcf1be682 Mon Sep 17 00:00:00 2001 From: Toni Date: Mon, 15 Nov 2021 17:21:59 +0200 Subject: [PATCH 12/21] hotfix for too many database writes --- src/metax_api/services/rabbitmq_service.py | 19 +++++++++---- src/metax_api/signals/post_delete.py | 33 +++++++++++----------- src/metax_api/tests/models/signals.py | 10 ------- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/metax_api/services/rabbitmq_service.py b/src/metax_api/services/rabbitmq_service.py index 0e541693..bc2dbe4f 100755 --- a/src/metax_api/services/rabbitmq_service.py +++ b/src/metax_api/services/rabbitmq_service.py @@ -118,22 +118,29 @@ def consume_api_errors(self): channel = connection.channel() try: + errors = [] for method, _, body in channel.consume("metax-apierrors", inactivity_timeout=1): if method is None and body is None: channel.cancel() break try: - error = loads(body) - ApiError.objects.create(identifier=error["identifier"], error=error) - except DatabaseError as e: - _logger.error("cannot create API Error. Discarding..") - _logger.debug(f"error: {e}") + error_payload = loads(body) + error = ApiError(identifier=error_payload["identifier"], error=error_payload) + errors.append(error) + except Exception as e: + _logger.error(e) finally: channel.basic_ack(method.delivery_tag) + try: + ApiError.objects.bulk_create(errors, batch_size=5000) + except DatabaseError as e: + _logger.error("cannot create API Error. Discarding..") + _logger.error(f"error: {e}") + except Exception as e: _logger.error(e) finally: - _logger.debug("All ApiErrors were handled") + _logger.info("All ApiErrors were handled") connection.close() def init_exchanges(self): diff --git a/src/metax_api/signals/post_delete.py b/src/metax_api/signals/post_delete.py index 1ff39f75..3dd1a0f5 100644 --- a/src/metax_api/signals/post_delete.py +++ b/src/metax_api/signals/post_delete.py @@ -5,23 +5,24 @@ from django.dispatch import receiver from django.core.serializers.json import DjangoJSONEncoder from django.forms.models import model_to_dict -from ..models import DeletedObject +from ..models import DeletedObject, CatalogRecord, CatalogRecordV2 _logger = logging.getLogger(__name__) @receiver(post_delete) -def deleted_object_receiver(instance, *args, **kwargs): - try: - model_type = instance._meta.model.__name__ - if hasattr(instance, '_initial_data["date_created"]'): - instance._initial_data["date_created"] = instance._initial_data["date_created"].strftime("%m/%d/%Y, %H:%M:%S") - if hasattr(instance, 'date_created'): - instance.date_created = instance.date_created.strftime("%m/%d/%Y, %H:%M:%S") - if hasattr(instance, 'date_modified'): - instance.date_modified = instance.date_modified.strftime("%m/%d/%Y, %H:%M:%S") - instance = model_to_dict(instance) - deleted_object_json = json.dumps(instance, cls=DjangoJSONEncoder) - DeletedObject.objects.create(model_name=model_type, object_data=deleted_object_json) - except Exception as e: - _logger.error("cannot save Deleted Object. Discarding..") - _logger.debug(f"error: {e}") \ No newline at end of file +def deleted_object_receiver(instance, sender, *args, **kwargs): + if sender in [CatalogRecord, CatalogRecordV2]: + try: + model_type = instance._meta.model.__name__ + if hasattr(instance, '_initial_data["date_created"]'): + instance._initial_data["date_created"] = instance._initial_data["date_created"].strftime("%m/%d/%Y, %H:%M:%S") + if hasattr(instance, 'date_created'): + instance.date_created = instance.date_created.strftime("%m/%d/%Y, %H:%M:%S") + if hasattr(instance, 'date_modified'): + instance.date_modified = instance.date_modified.strftime("%m/%d/%Y, %H:%M:%S") + instance = model_to_dict(instance) + deleted_object_json = json.dumps(instance, cls=DjangoJSONEncoder) + DeletedObject.objects.create(model_name=model_type, object_data=deleted_object_json) + except Exception as e: + _logger.error("cannot save Deleted Object. Discarding..") + _logger.debug(f"error: {e}") \ No newline at end of file diff --git a/src/metax_api/tests/models/signals.py b/src/metax_api/tests/models/signals.py index 548474ce..b9c11aae 100644 --- a/src/metax_api/tests/models/signals.py +++ b/src/metax_api/tests/models/signals.py @@ -33,14 +33,4 @@ def test_deleting_catalog_record_creates_new_deleted_object(self): self.assertEqual(deleted_object_v2.model_name, "CatalogRecordV2") self.assertEqual(deleted_object_v2.date_deleted.strftime("%d/%m/%Y"), self.today) - def test_deleting_file_creates_new_deleted_object(self): - File.objects.get(pk=1).delete(hard=True) - deleted_object = DeletedObject.objects.last() - self.assertEqual(deleted_object.model_name, "File") - self.assertEqual(deleted_object.date_deleted.strftime("%d/%m/%Y"), self.today) - def test_deleting_directory_creates_new_deleted_object(self): - Directory.objects.get(pk=1).delete() - deleted_object = DeletedObject.objects.last() - self.assertEqual(deleted_object.model_name, "Directory") - self.assertEqual(deleted_object.date_deleted.strftime("%d/%m/%Y"), self.today) \ No newline at end of file From d5a29c8aa0b431c76a3a43865900bb91788cbed8 Mon Sep 17 00:00:00 2001 From: Toni Date: Mon, 15 Nov 2021 17:21:59 +0200 Subject: [PATCH 13/21] hotfix for too many database writes --- src/metax_api/services/rabbitmq_service.py | 19 +++++++++---- src/metax_api/signals/post_delete.py | 33 +++++++++++----------- src/metax_api/tests/models/signals.py | 10 ------- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/metax_api/services/rabbitmq_service.py b/src/metax_api/services/rabbitmq_service.py index 0e541693..bc2dbe4f 100755 --- a/src/metax_api/services/rabbitmq_service.py +++ b/src/metax_api/services/rabbitmq_service.py @@ -118,22 +118,29 @@ def consume_api_errors(self): channel = connection.channel() try: + errors = [] for method, _, body in channel.consume("metax-apierrors", inactivity_timeout=1): if method is None and body is None: channel.cancel() break try: - error = loads(body) - ApiError.objects.create(identifier=error["identifier"], error=error) - except DatabaseError as e: - _logger.error("cannot create API Error. Discarding..") - _logger.debug(f"error: {e}") + error_payload = loads(body) + error = ApiError(identifier=error_payload["identifier"], error=error_payload) + errors.append(error) + except Exception as e: + _logger.error(e) finally: channel.basic_ack(method.delivery_tag) + try: + ApiError.objects.bulk_create(errors, batch_size=5000) + except DatabaseError as e: + _logger.error("cannot create API Error. Discarding..") + _logger.error(f"error: {e}") + except Exception as e: _logger.error(e) finally: - _logger.debug("All ApiErrors were handled") + _logger.info("All ApiErrors were handled") connection.close() def init_exchanges(self): diff --git a/src/metax_api/signals/post_delete.py b/src/metax_api/signals/post_delete.py index 1ff39f75..3dd1a0f5 100644 --- a/src/metax_api/signals/post_delete.py +++ b/src/metax_api/signals/post_delete.py @@ -5,23 +5,24 @@ from django.dispatch import receiver from django.core.serializers.json import DjangoJSONEncoder from django.forms.models import model_to_dict -from ..models import DeletedObject +from ..models import DeletedObject, CatalogRecord, CatalogRecordV2 _logger = logging.getLogger(__name__) @receiver(post_delete) -def deleted_object_receiver(instance, *args, **kwargs): - try: - model_type = instance._meta.model.__name__ - if hasattr(instance, '_initial_data["date_created"]'): - instance._initial_data["date_created"] = instance._initial_data["date_created"].strftime("%m/%d/%Y, %H:%M:%S") - if hasattr(instance, 'date_created'): - instance.date_created = instance.date_created.strftime("%m/%d/%Y, %H:%M:%S") - if hasattr(instance, 'date_modified'): - instance.date_modified = instance.date_modified.strftime("%m/%d/%Y, %H:%M:%S") - instance = model_to_dict(instance) - deleted_object_json = json.dumps(instance, cls=DjangoJSONEncoder) - DeletedObject.objects.create(model_name=model_type, object_data=deleted_object_json) - except Exception as e: - _logger.error("cannot save Deleted Object. Discarding..") - _logger.debug(f"error: {e}") \ No newline at end of file +def deleted_object_receiver(instance, sender, *args, **kwargs): + if sender in [CatalogRecord, CatalogRecordV2]: + try: + model_type = instance._meta.model.__name__ + if hasattr(instance, '_initial_data["date_created"]'): + instance._initial_data["date_created"] = instance._initial_data["date_created"].strftime("%m/%d/%Y, %H:%M:%S") + if hasattr(instance, 'date_created'): + instance.date_created = instance.date_created.strftime("%m/%d/%Y, %H:%M:%S") + if hasattr(instance, 'date_modified'): + instance.date_modified = instance.date_modified.strftime("%m/%d/%Y, %H:%M:%S") + instance = model_to_dict(instance) + deleted_object_json = json.dumps(instance, cls=DjangoJSONEncoder) + DeletedObject.objects.create(model_name=model_type, object_data=deleted_object_json) + except Exception as e: + _logger.error("cannot save Deleted Object. Discarding..") + _logger.debug(f"error: {e}") \ No newline at end of file diff --git a/src/metax_api/tests/models/signals.py b/src/metax_api/tests/models/signals.py index 548474ce..b9c11aae 100644 --- a/src/metax_api/tests/models/signals.py +++ b/src/metax_api/tests/models/signals.py @@ -33,14 +33,4 @@ def test_deleting_catalog_record_creates_new_deleted_object(self): self.assertEqual(deleted_object_v2.model_name, "CatalogRecordV2") self.assertEqual(deleted_object_v2.date_deleted.strftime("%d/%m/%Y"), self.today) - def test_deleting_file_creates_new_deleted_object(self): - File.objects.get(pk=1).delete(hard=True) - deleted_object = DeletedObject.objects.last() - self.assertEqual(deleted_object.model_name, "File") - self.assertEqual(deleted_object.date_deleted.strftime("%d/%m/%Y"), self.today) - def test_deleting_directory_creates_new_deleted_object(self): - Directory.objects.get(pk=1).delete() - deleted_object = DeletedObject.objects.last() - self.assertEqual(deleted_object.model_name, "Directory") - self.assertEqual(deleted_object.date_deleted.strftime("%d/%m/%Y"), self.today) \ No newline at end of file From 3373dfddcbadedb44f7f3600c5b63f3c1d22149c Mon Sep 17 00:00:00 2001 From: Toni Date: Fri, 19 Nov 2021 14:55:29 +0200 Subject: [PATCH 14/21] log deleted objects instead of saving them to database, write apierrors to file instead of database for now. --- src/metax_api/services/rabbitmq_service.py | 11 +++++++++-- src/metax_api/settings/__init__.py | 1 + src/metax_api/settings/components/common.py | 3 +++ src/metax_api/signals/post_delete.py | 8 ++++++-- src/metax_api/tests/models/signals.py | 4 ++-- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/metax_api/services/rabbitmq_service.py b/src/metax_api/services/rabbitmq_service.py index bc2dbe4f..a4192717 100755 --- a/src/metax_api/services/rabbitmq_service.py +++ b/src/metax_api/services/rabbitmq_service.py @@ -7,16 +7,19 @@ import logging import random +from datetime import datetime from json import dumps as json_dumps, loads from time import sleep import pika + +from django.core import serializers from django.db import DatabaseError from django.conf import settings from django.core.serializers.json import DjangoJSONEncoder from metax_api.models import ApiError -from metax_api.utils.utils import executing_test_case +from metax_api.utils.utils import executing_test_case, datetime_to_str, parse_timestamp_string_to_tz_aware_datetime _logger = logging.getLogger(__name__) @@ -132,7 +135,11 @@ def consume_api_errors(self): finally: channel.basic_ack(method.delivery_tag) try: - ApiError.objects.bulk_create(errors, batch_size=5000) + # ApiError.objects.bulk_create(errors, batch_size=5000) + now = datetime.now() + tz_aware = parse_timestamp_string_to_tz_aware_datetime(datetime_to_str(now)) + with open(f"/var/log/metax-api/errors/api-errors/{tz_aware}") as out: + serializers.serialize("json", errors, stream=out) except DatabaseError as e: _logger.error("cannot create API Error. Discarding..") _logger.error(f"error: {e}") diff --git a/src/metax_api/settings/__init__.py b/src/metax_api/settings/__init__.py index b091c712..c9950152 100755 --- a/src/metax_api/settings/__init__.py +++ b/src/metax_api/settings/__init__.py @@ -28,6 +28,7 @@ ELASTIC_SEARCH_HOSTS=(list, ["localhost"]), ELASTIC_SEARCH_PORT=(int, 9200), ELASTIC_SEARCH_USE_SSL=(bool, False), + ENABLE_DELETED_OBJECTS_SAVING=(bool, False), ENABLE_V1_ENDPOINTS=(bool, True), ENABLE_V2_ENDPOINTS=(bool, True), ENABLE_DJANGO_WATCHMAN=(bool, False), diff --git a/src/metax_api/settings/components/common.py b/src/metax_api/settings/components/common.py index 24529eb2..972d5272 100755 --- a/src/metax_api/settings/components/common.py +++ b/src/metax_api/settings/components/common.py @@ -28,6 +28,9 @@ CHECKSUM_ALGORITHMS = ["SHA-256", "MD5", "SHA-512"] ERROR_FILES_PATH = env("ERROR_FILES_PATH") +ENABLE_DELETED_OBJECTS_SAVING = env("ENABLE_DELETED_OBJECTS_SAVING") + + # Allow only specific hosts to access the app ALLOWED_HOSTS = ["localhost", "127.0.0.1", "[::1]"] diff --git a/src/metax_api/signals/post_delete.py b/src/metax_api/signals/post_delete.py index 3dd1a0f5..ccce7b4e 100644 --- a/src/metax_api/signals/post_delete.py +++ b/src/metax_api/signals/post_delete.py @@ -1,6 +1,7 @@ import json import logging +from django.conf import settings from django.db.models.signals import post_delete from django.dispatch import receiver from django.core.serializers.json import DjangoJSONEncoder @@ -21,8 +22,11 @@ def deleted_object_receiver(instance, sender, *args, **kwargs): if hasattr(instance, 'date_modified'): instance.date_modified = instance.date_modified.strftime("%m/%d/%Y, %H:%M:%S") instance = model_to_dict(instance) - deleted_object_json = json.dumps(instance, cls=DjangoJSONEncoder) - DeletedObject.objects.create(model_name=model_type, object_data=deleted_object_json) + if settings.ENABLE_DELETED_OBJECTS_SAVING: + deleted_object_json = json.dumps(instance, cls=DjangoJSONEncoder) + DeletedObject.objects.create(model_name=model_type, object_data=deleted_object_json) + else: + _logger.info(str(instance)) except Exception as e: _logger.error("cannot save Deleted Object. Discarding..") _logger.debug(f"error: {e}") \ No newline at end of file diff --git a/src/metax_api/tests/models/signals.py b/src/metax_api/tests/models/signals.py index b9c11aae..3e3b57e3 100644 --- a/src/metax_api/tests/models/signals.py +++ b/src/metax_api/tests/models/signals.py @@ -20,7 +20,7 @@ def setUp(self): call_command("loaddata", test_data_file_path, verbosity=0) self.today = date.today().strftime("%d/%m/%Y") - def test_deleting_catalog_record_creates_new_deleted_object(self): +""" def test_deleting_catalog_record_creates_new_deleted_object(self): # test that deleting CatalogRecord object creates a new deleted object CatalogRecord.objects_unfiltered.get(pk=1).delete(hard=True) deleted_object = DeletedObject.objects.last() @@ -31,6 +31,6 @@ def test_deleting_catalog_record_creates_new_deleted_object(self): CatalogRecordV2.objects_unfiltered.get(pk=2).delete(hard=True) deleted_object_v2 = DeletedObject.objects.last() self.assertEqual(deleted_object_v2.model_name, "CatalogRecordV2") - self.assertEqual(deleted_object_v2.date_deleted.strftime("%d/%m/%Y"), self.today) + self.assertEqual(deleted_object_v2.date_deleted.strftime("%d/%m/%Y"), self.today)""" From 57a782ce5375d8dc1c27eaaab96640c8a6a74545 Mon Sep 17 00:00:00 2001 From: Toni Date: Fri, 26 Nov 2021 14:43:25 +0200 Subject: [PATCH 15/21] put api-error objects behind settings flag, disable signal and api-error tests if they are not enabled. --- src/metax_api/api/rest/base/views/common_view.py | 11 +++++++++-- src/metax_api/onappstart.py | 3 ++- src/metax_api/settings/__init__.py | 2 ++ src/metax_api/settings/components/common.py | 2 ++ src/metax_api/signals/__init__.py | 7 +++++-- .../tests/api/rest/v2/views/apierrors/read.py | 3 +++ src/metax_api/tests/models/signals.py | 10 ++++++---- 7 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/metax_api/api/rest/base/views/common_view.py b/src/metax_api/api/rest/base/views/common_view.py index 14de18ca..f5f0be75 100755 --- a/src/metax_api/api/rest/base/views/common_view.py +++ b/src/metax_api/api/rest/base/views/common_view.py @@ -8,6 +8,7 @@ import logging from os import path +from django.conf import settings from django.http import Http404, HttpResponse from rest_framework import status from rest_framework.exceptions import APIException, MethodNotAllowed, PermissionDenied @@ -141,7 +142,10 @@ def handle_exception(self, exc): try: error_json = ApiErrorSerializerV2.request_to_json(self.request, response) response.data["error_identifier"] = error_json["identifier"] - rabbitmq.publish(error_json, exchange="apierrors") + if settings.ENABLE_API_ERROR_OBJECTS: + rabbitmq.publish(error_json, exchange="apierrors") + else: + _logger.error(f"api error: {str(error_json)}") except Exception as e: _logger.error(f"could not send api error to rabbitmq. Error: {e}") @@ -375,7 +379,10 @@ def _check_and_store_bulk_error(self, request, response): try: error_json = ApiErrorSerializerV2.request_to_json(self.request, response, other={"bulk_request": True}) response.data["error_identifier"] = error_json["identifier"] - rabbitmq.publish(error_json, exchange="apierrors") + if settings.ENABLE_API_ERROR_OBJECTS: + rabbitmq.publish(error_json, exchange="apierrors") + else: + _logger.error(f"api error: {str(error_json)}") except Exception as e: _logger.error(f"could not send api error to rabbitmq. Error: {e}") diff --git a/src/metax_api/onappstart.py b/src/metax_api/onappstart.py index 2a0346c6..a54b4a61 100755 --- a/src/metax_api/onappstart.py +++ b/src/metax_api/onappstart.py @@ -40,7 +40,8 @@ def ready(self): # pragma: no cover # because the "django apps" have not been loaded yet. import json - import metax_api.signals # noqa + if settings.ENABLE_SIGNALS: + import metax_api.signals # noqa from metax_api.services import RabbitMQService as rabbitmq from metax_api.services.redis_cache_service import RedisClient diff --git a/src/metax_api/settings/__init__.py b/src/metax_api/settings/__init__.py index c9950152..6dc65e87 100755 --- a/src/metax_api/settings/__init__.py +++ b/src/metax_api/settings/__init__.py @@ -28,7 +28,9 @@ ELASTIC_SEARCH_HOSTS=(list, ["localhost"]), ELASTIC_SEARCH_PORT=(int, 9200), ELASTIC_SEARCH_USE_SSL=(bool, False), + ENABLE_API_ERROR_OBJECTS=(bool, False), ENABLE_DELETED_OBJECTS_SAVING=(bool, False), + ENABLE_SIGNALS=(bool, False), ENABLE_V1_ENDPOINTS=(bool, True), ENABLE_V2_ENDPOINTS=(bool, True), ENABLE_DJANGO_WATCHMAN=(bool, False), diff --git a/src/metax_api/settings/components/common.py b/src/metax_api/settings/components/common.py index 972d5272..e422aeda 100755 --- a/src/metax_api/settings/components/common.py +++ b/src/metax_api/settings/components/common.py @@ -29,6 +29,8 @@ ERROR_FILES_PATH = env("ERROR_FILES_PATH") ENABLE_DELETED_OBJECTS_SAVING = env("ENABLE_DELETED_OBJECTS_SAVING") +ENABLE_SIGNALS = env("ENABLE_SIGNALS") +ENABLE_API_ERROR_OBJECTS = env("ENABLE_API_ERROR_OBJECTS") # Allow only specific hosts to access the app diff --git a/src/metax_api/signals/__init__.py b/src/metax_api/signals/__init__.py index 3c389606..f53b3d98 100644 --- a/src/metax_api/signals/__init__.py +++ b/src/metax_api/signals/__init__.py @@ -1,2 +1,5 @@ -from .post_delete import * -from .request_finished import * \ No newline at end of file +from django.conf import settings + +if settings.ENABLE_SIGNALS: + from .post_delete import * + from .request_finished import * \ No newline at end of file diff --git a/src/metax_api/tests/api/rest/v2/views/apierrors/read.py b/src/metax_api/tests/api/rest/v2/views/apierrors/read.py index ce8965e7..5d2d0d06 100755 --- a/src/metax_api/tests/api/rest/v2/views/apierrors/read.py +++ b/src/metax_api/tests/api/rest/v2/views/apierrors/read.py @@ -5,6 +5,7 @@ # :author: CSC - IT Center for Science Ltd., Espoo Finland # :license: MIT import logging +import unittest from unittest.mock import patch from uuid import uuid4 @@ -14,10 +15,12 @@ from metax_api.models import ApiError from metax_api.tests.utils import TestClassUtils, test_data_file_path, testcase_log_console +from django.conf import settings _logger = logging.getLogger(__name__) +@unittest.skipIf(settings.ENABLE_API_ERROR_OBJECTS is not True, "Only run if API errors are objects") class ApiErrorReadBasicTests(APITestCase, TestClassUtils): """ diff --git a/src/metax_api/tests/models/signals.py b/src/metax_api/tests/models/signals.py index 3e3b57e3..08cf9947 100644 --- a/src/metax_api/tests/models/signals.py +++ b/src/metax_api/tests/models/signals.py @@ -6,21 +6,23 @@ # :license: MIT from datetime import date - +import unittest from django.core.management import call_command from django.test import TestCase +from django.conf import settings -from metax_api.models import CatalogRecord, CatalogRecordV2, DeletedObject, Directory, File +from metax_api.models import CatalogRecord, CatalogRecordV2, DeletedObject from metax_api.tests.utils import TestClassUtils, test_data_file_path +@unittest.skipIf(settings.ENABLE_DELETED_OBJECTS_SAVING is not True, "Only run if deleted objects are saved") class SignalTests(TestCase, TestClassUtils): def setUp(self): call_command("loaddata", test_data_file_path, verbosity=0) self.today = date.today().strftime("%d/%m/%Y") -""" def test_deleting_catalog_record_creates_new_deleted_object(self): + def test_deleting_catalog_record_creates_new_deleted_object(self): # test that deleting CatalogRecord object creates a new deleted object CatalogRecord.objects_unfiltered.get(pk=1).delete(hard=True) deleted_object = DeletedObject.objects.last() @@ -31,6 +33,6 @@ def setUp(self): CatalogRecordV2.objects_unfiltered.get(pk=2).delete(hard=True) deleted_object_v2 = DeletedObject.objects.last() self.assertEqual(deleted_object_v2.model_name, "CatalogRecordV2") - self.assertEqual(deleted_object_v2.date_deleted.strftime("%d/%m/%Y"), self.today)""" + self.assertEqual(deleted_object_v2.date_deleted.strftime("%d/%m/%Y"), self.today) From 81b6058517c4cfe8fffe650ddb1dd411454d624f Mon Sep 17 00:00:00 2001 From: Atro Himanen Date: Wed, 24 Nov 2021 11:46:47 +0200 Subject: [PATCH 16/21] Added management command for marking files as removed --- .../management/commands/mark_files_removed.py | 49 +++++++++++++++++++ .../management/commands/mark_files_removed.py | 35 +++++++++++++ src/metax_api/tests/utils.py | 24 ++++++++- 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/metax_api/management/commands/mark_files_removed.py create mode 100644 src/metax_api/tests/management/commands/mark_files_removed.py diff --git a/src/metax_api/management/commands/mark_files_removed.py b/src/metax_api/management/commands/mark_files_removed.py new file mode 100644 index 00000000..0f5de913 --- /dev/null +++ b/src/metax_api/management/commands/mark_files_removed.py @@ -0,0 +1,49 @@ +import logging + +from django.core.management.base import BaseCommand +from metax_api.models import File +from metax_api.tests.utils import management_command_add_test_logs + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + help = """Marks files which start with a given file path removed from a given project. + File path prefix can be given as command line parameter or they can be read from a file. + This command will produce duplicate prints, the first line is used in tests, but is not stored on logs. + The second line is stored on logs, but can't be used by the tests.""" + + def add_arguments(self, parser): + parser.add_argument("project_identifier", type=str, help="Identifier of the project where the files are removed") + parser.add_argument("--path_prefix", type=str, help="Prefix for the file path of the files that are removed") + parser.add_argument("--path_prefix_file", type=str, help="Name of the file where to read the path prefixes (Required if --path-prefix is not given)") + + @management_command_add_test_logs(logger) + def handle(self, *args, **options): + logger.info("Remove files from database") + if options["path_prefix"] is None and options["path_prefix_file"] is None: + logger.error("path_prefix or path_prefix_file is required") + return + + if options["path_prefix_file"]: + path_prefixes = self.read_prefixes_from_file(options["path_prefix_file"]) + else: + path_prefixes = [options["path_prefix"]] + + + removed_files_sum = 0 + for prefix in path_prefixes: + files = File.objects.filter(project_identifier = options["project_identifier"], file_path__startswith = prefix, removed = "f") + logger.info("Found %d files to remove in project: %s with path prefix: %s" % (len(files), options["project_identifier"], prefix)) + for file in files: + file.delete() + removed_files_sum += len(files) + + logger.info("Removed %d files" % removed_files_sum) + + + + def read_prefixes_from_file(self, filename): + with open(filename) as file: + lines = file.readlines() + lines = [line.rstrip() for line in lines] + return lines diff --git a/src/metax_api/tests/management/commands/mark_files_removed.py b/src/metax_api/tests/management/commands/mark_files_removed.py new file mode 100644 index 00000000..1b401108 --- /dev/null +++ b/src/metax_api/tests/management/commands/mark_files_removed.py @@ -0,0 +1,35 @@ +from io import StringIO +import logging + +from django.core.management import call_command +from django.test import TestCase + +from metax_api.models import File +from metax_api.tests.utils import test_data_file_path +from metax_api.management.commands.mark_files_removed import Command + +_logger = logging.getLogger(__name__) + +class RemoveFilesTest(TestCase): + + def setUp(self): + call_command("loaddata", test_data_file_path, verbosity=0) + + def test_command_output(self): + project_identifier = "project_x" + path_prefix = "/project_x_FROZEN/Experiment_X/Phase_1/2017" + path_prefix_file = None + + out = StringIO() + args = [project_identifier] + options = {"path_prefix": path_prefix, "stdout": out} + call_command('mark_files_removed', *args, **options) + + self.assertIn('Found 10 files to remove in project: ' + project_identifier + ' with path prefix: ' + path_prefix, out.getvalue()) + self.assertIn('Removed 10 files', out.getvalue()) + + files = File.objects_unfiltered.filter(project_identifier = project_identifier, file_path__startswith = path_prefix) + + self.assertEqual(10, len(files)) + for file in files: + self.assertTrue(file.removed) \ No newline at end of file diff --git a/src/metax_api/tests/utils.py b/src/metax_api/tests/utils.py index 6fd25b6a..d4cbb3e0 100755 --- a/src/metax_api/tests/utils.py +++ b/src/metax_api/tests/utils.py @@ -9,6 +9,7 @@ from base64 import b64encode from contextlib import contextmanager from json import load as json_load +from io import StringIO from os import path import jwt @@ -456,10 +457,15 @@ def _get_new_file_data( @contextmanager -def streamhandler_to_console(lggr): +def streamhandler_to_console(lggr, command_obj = None): # Use 'up to date' value of sys.stdout for StreamHandler, # as set by test runner. - stream_handler = logging.StreamHandler(sys.stdout) + # If command_obj is given, use OutputWrapper of the given + # Command object + if command_obj == None: + stream_handler = logging.StreamHandler(sys.stdout) + else: + stream_handler = logging.StreamHandler(command_obj.stdout) lggr.addHandler(stream_handler) yield lggr.removeHandler(stream_handler) @@ -474,3 +480,17 @@ def testcase_log_console(*args, **kwargs): return testcase_log_console return testcase_decorator + +def management_command_add_test_logs(lggr): + # This decorator can be used by management commands to capture + # the logs of the command in a format that can be used in tests + def management_command_log_decorator(func): + def management_command_add_test_logs(*args, **kwargs): + # args[0] is the object which uses this decorator (typically a command object) + with streamhandler_to_console(lggr, args[0]): + return func(*args, **kwargs) + + return management_command_add_test_logs + + return management_command_log_decorator + From 7758ab9cb35c99627ca9460d9ce0727211b94832 Mon Sep 17 00:00:00 2001 From: Atro Himanen Date: Thu, 25 Nov 2021 09:39:15 +0200 Subject: [PATCH 17/21] Updated string format syntax --- src/metax_api/management/commands/mark_files_removed.py | 4 ++-- src/metax_api/tests/management/commands/mark_files_removed.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/metax_api/management/commands/mark_files_removed.py b/src/metax_api/management/commands/mark_files_removed.py index 0f5de913..22c9ec86 100644 --- a/src/metax_api/management/commands/mark_files_removed.py +++ b/src/metax_api/management/commands/mark_files_removed.py @@ -33,12 +33,12 @@ def handle(self, *args, **options): removed_files_sum = 0 for prefix in path_prefixes: files = File.objects.filter(project_identifier = options["project_identifier"], file_path__startswith = prefix, removed = "f") - logger.info("Found %d files to remove in project: %s with path prefix: %s" % (len(files), options["project_identifier"], prefix)) + logger.info(f"Found {len(files)} files to remove in project: {options['project_identifier']} with path prefix: {prefix}") for file in files: file.delete() removed_files_sum += len(files) - logger.info("Removed %d files" % removed_files_sum) + logger.info(f"Removed {removed_files_sum} files") diff --git a/src/metax_api/tests/management/commands/mark_files_removed.py b/src/metax_api/tests/management/commands/mark_files_removed.py index 1b401108..4cb0eaaf 100644 --- a/src/metax_api/tests/management/commands/mark_files_removed.py +++ b/src/metax_api/tests/management/commands/mark_files_removed.py @@ -25,7 +25,7 @@ def test_command_output(self): options = {"path_prefix": path_prefix, "stdout": out} call_command('mark_files_removed', *args, **options) - self.assertIn('Found 10 files to remove in project: ' + project_identifier + ' with path prefix: ' + path_prefix, out.getvalue()) + self.assertIn(f'Found 10 files to remove in project: {project_identifier} with path prefix: {path_prefix}', out.getvalue()) self.assertIn('Removed 10 files', out.getvalue()) files = File.objects_unfiltered.filter(project_identifier = project_identifier, file_path__startswith = path_prefix) From 563136fe5fedc4b043fca1b966bef30fe8e09523 Mon Sep 17 00:00:00 2001 From: tonurmi Date: Wed, 8 Dec 2021 16:40:06 +0200 Subject: [PATCH 18/21] Production update 21/12/1 Mark files removed script --- .../management/commands/mark_files_removed.py | 49 +++++++++++++++++++ .../management/commands/mark_files_removed.py | 35 +++++++++++++ src/metax_api/tests/utils.py | 24 ++++++++- 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/metax_api/management/commands/mark_files_removed.py create mode 100644 src/metax_api/tests/management/commands/mark_files_removed.py diff --git a/src/metax_api/management/commands/mark_files_removed.py b/src/metax_api/management/commands/mark_files_removed.py new file mode 100644 index 00000000..22c9ec86 --- /dev/null +++ b/src/metax_api/management/commands/mark_files_removed.py @@ -0,0 +1,49 @@ +import logging + +from django.core.management.base import BaseCommand +from metax_api.models import File +from metax_api.tests.utils import management_command_add_test_logs + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + help = """Marks files which start with a given file path removed from a given project. + File path prefix can be given as command line parameter or they can be read from a file. + This command will produce duplicate prints, the first line is used in tests, but is not stored on logs. + The second line is stored on logs, but can't be used by the tests.""" + + def add_arguments(self, parser): + parser.add_argument("project_identifier", type=str, help="Identifier of the project where the files are removed") + parser.add_argument("--path_prefix", type=str, help="Prefix for the file path of the files that are removed") + parser.add_argument("--path_prefix_file", type=str, help="Name of the file where to read the path prefixes (Required if --path-prefix is not given)") + + @management_command_add_test_logs(logger) + def handle(self, *args, **options): + logger.info("Remove files from database") + if options["path_prefix"] is None and options["path_prefix_file"] is None: + logger.error("path_prefix or path_prefix_file is required") + return + + if options["path_prefix_file"]: + path_prefixes = self.read_prefixes_from_file(options["path_prefix_file"]) + else: + path_prefixes = [options["path_prefix"]] + + + removed_files_sum = 0 + for prefix in path_prefixes: + files = File.objects.filter(project_identifier = options["project_identifier"], file_path__startswith = prefix, removed = "f") + logger.info(f"Found {len(files)} files to remove in project: {options['project_identifier']} with path prefix: {prefix}") + for file in files: + file.delete() + removed_files_sum += len(files) + + logger.info(f"Removed {removed_files_sum} files") + + + + def read_prefixes_from_file(self, filename): + with open(filename) as file: + lines = file.readlines() + lines = [line.rstrip() for line in lines] + return lines diff --git a/src/metax_api/tests/management/commands/mark_files_removed.py b/src/metax_api/tests/management/commands/mark_files_removed.py new file mode 100644 index 00000000..4cb0eaaf --- /dev/null +++ b/src/metax_api/tests/management/commands/mark_files_removed.py @@ -0,0 +1,35 @@ +from io import StringIO +import logging + +from django.core.management import call_command +from django.test import TestCase + +from metax_api.models import File +from metax_api.tests.utils import test_data_file_path +from metax_api.management.commands.mark_files_removed import Command + +_logger = logging.getLogger(__name__) + +class RemoveFilesTest(TestCase): + + def setUp(self): + call_command("loaddata", test_data_file_path, verbosity=0) + + def test_command_output(self): + project_identifier = "project_x" + path_prefix = "/project_x_FROZEN/Experiment_X/Phase_1/2017" + path_prefix_file = None + + out = StringIO() + args = [project_identifier] + options = {"path_prefix": path_prefix, "stdout": out} + call_command('mark_files_removed', *args, **options) + + self.assertIn(f'Found 10 files to remove in project: {project_identifier} with path prefix: {path_prefix}', out.getvalue()) + self.assertIn('Removed 10 files', out.getvalue()) + + files = File.objects_unfiltered.filter(project_identifier = project_identifier, file_path__startswith = path_prefix) + + self.assertEqual(10, len(files)) + for file in files: + self.assertTrue(file.removed) \ No newline at end of file diff --git a/src/metax_api/tests/utils.py b/src/metax_api/tests/utils.py index 6fd25b6a..d4cbb3e0 100755 --- a/src/metax_api/tests/utils.py +++ b/src/metax_api/tests/utils.py @@ -9,6 +9,7 @@ from base64 import b64encode from contextlib import contextmanager from json import load as json_load +from io import StringIO from os import path import jwt @@ -456,10 +457,15 @@ def _get_new_file_data( @contextmanager -def streamhandler_to_console(lggr): +def streamhandler_to_console(lggr, command_obj = None): # Use 'up to date' value of sys.stdout for StreamHandler, # as set by test runner. - stream_handler = logging.StreamHandler(sys.stdout) + # If command_obj is given, use OutputWrapper of the given + # Command object + if command_obj == None: + stream_handler = logging.StreamHandler(sys.stdout) + else: + stream_handler = logging.StreamHandler(command_obj.stdout) lggr.addHandler(stream_handler) yield lggr.removeHandler(stream_handler) @@ -474,3 +480,17 @@ def testcase_log_console(*args, **kwargs): return testcase_log_console return testcase_decorator + +def management_command_add_test_logs(lggr): + # This decorator can be used by management commands to capture + # the logs of the command in a format that can be used in tests + def management_command_log_decorator(func): + def management_command_add_test_logs(*args, **kwargs): + # args[0] is the object which uses this decorator (typically a command object) + with streamhandler_to_console(lggr, args[0]): + return func(*args, **kwargs) + + return management_command_add_test_logs + + return management_command_log_decorator + From c8d7f1d9571663fb3859f5040456c23ccc4402d4 Mon Sep 17 00:00:00 2001 From: tonurmi Date: Tue, 1 Feb 2022 14:22:38 +0200 Subject: [PATCH 19/21] Update to version 2.7.0 --- .gitlab-ci.yml | 14 +- ENV_VARS.md | 1 + docker-compose.yml | 2 + poetry.lock | 713 ++++++++++-------- pyproject.toml | 3 +- requirements.txt | 92 ++- src/metax_api/api/rest/base/router.py | 6 + .../api/rest/base/serializers/__init__.py | 1 + .../serializers/data_catalog_serializer.py | 2 + .../editor_permissions_serializer.py | 20 + src/metax_api/api/rest/base/views/__init__.py | 1 + .../api/rest/base/views/common_view.py | 8 +- .../api/rest/base/views/dataset_view.py | 13 +- .../api/rest/base/views/directory_view.py | 2 +- .../base/views/editor_permissions_view.py | 144 ++++ src/metax_api/api/rest/v2/router.py | 6 + .../api/rest/v2/views/api_error_view.py | 2 +- .../api/rest/v2/views/dataset_view.py | 2 +- .../api/rpc/base/views/dataset_rpc.py | 42 ++ src/metax_api/api/rpc/base/views/file_rpc.py | 5 +- .../api/rpc/base/views/statistic_rpc.py | 5 + src/metax_api/initialdata/datacatalogs.json | 288 ++++++- .../management/commands/mark_files_removed.py | 3 + src/metax_api/middleware/identifyapicaller.py | 12 +- .../middleware/stream_http_response.py | 5 +- .../migrations/0041_add_editorpermissions.py | 195 +++++ .../migrations/0042_auto_20211108_1256.py | 21 + ...43_remove_editoruserpermission_verified.py | 17 + .../0044_change_aalto_catalog_to_harvested.py | 34 + .../0045_add_publish_fields_to_catalogs.py | 50 ++ src/metax_api/models/__init__.py | 2 +- src/metax_api/models/catalog_record.py | 160 +++- src/metax_api/models/catalog_record_v2.py | 6 + src/metax_api/models/data_catalog.py | 7 + src/metax_api/models/directory.py | 108 +-- .../services/catalog_record_service.py | 20 + src/metax_api/services/file_service.py | 10 +- src/metax_api/services/rabbitmq_service.py | 5 +- src/metax_api/services/rems_service.py | 33 +- src/metax_api/services/statistic_service.py | 61 +- .../settings/components/access_control.py | 34 + src/metax_api/settings/components/common.py | 16 +- src/metax_api/settings/components/logging.py | 2 +- src/metax_api/settings/components/rems.py | 1 + .../settings/environments/production.py | 2 + src/metax_api/settings/environments/stable.py | 6 +- .../settings/environments/staging.py | 1 + .../settings/environments/unittests.py | 16 + src/metax_api/swagger/v1/swagger.yaml | 211 +++++- src/metax_api/swagger/v2/swagger.yaml | 205 ++++- .../tests/api/rest/base/views/common/read.py | 13 +- .../api/rest/base/views/datasets/read.py | 21 +- .../api/rest/base/views/datasets/write.py | 81 ++ .../base/views/editorpermissions/__init__.py | 2 + .../rest/base/views/editorpermissions/read.py | 110 +++ .../base/views/editorpermissions/write.py | 219 ++++++ .../tests/api/rest/base/views/schemas/read.py | 2 +- .../api/rest/v2/views/datasets/drafts.py | 16 + .../tests/api/rest/v2/views/datasets/write.py | 72 +- .../tests/api/rpc/base/views/dataset_rpc.py | 95 +++ .../tests/api/rpc/base/views/file_rpc.py | 71 +- .../tests/api/rpc/base/views/statistic_rpc.py | 58 ++ .../tests/api/rpc/v2/views/dataset_rpc.py | 15 + .../data_catalog_test_data_template.json | 6 +- .../tests/testdata/generate_test_data.py | 88 ++- src/metax_api/tests/testdata/test_data.json | 466 +++++++++++- 66 files changed, 3343 insertions(+), 607 deletions(-) create mode 100644 src/metax_api/api/rest/base/serializers/editor_permissions_serializer.py create mode 100644 src/metax_api/api/rest/base/views/editor_permissions_view.py create mode 100644 src/metax_api/migrations/0041_add_editorpermissions.py create mode 100644 src/metax_api/migrations/0042_auto_20211108_1256.py create mode 100644 src/metax_api/migrations/0043_remove_editoruserpermission_verified.py create mode 100755 src/metax_api/migrations/0044_change_aalto_catalog_to_harvested.py create mode 100644 src/metax_api/migrations/0045_add_publish_fields_to_catalogs.py create mode 100644 src/metax_api/tests/api/rest/base/views/editorpermissions/__init__.py create mode 100644 src/metax_api/tests/api/rest/base/views/editorpermissions/read.py create mode 100644 src/metax_api/tests/api/rest/base/views/editorpermissions/write.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 02300332..87e647dd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,13 +15,13 @@ deploy: stage: deploy environment: $CI_COMMIT_REF_NAME script: - - ansible-playbook -i $ANSIBLE_INVENTORY $DEPLOY_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA repo_version=$CI_COMMIT_REF_NAME" + - ansible-playbook -i $ANSIBLE_INVENTORY $DEPLOY_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" integration_test: stage: test environment: $CI_COMMIT_REF_NAME script: - - ansible-playbook -i $ANSIBLE_INVENTORY $TEST_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA" + - ansible-playbook -i $ANSIBLE_INVENTORY $TEST_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" clean_test: stage: clean_test @@ -29,7 +29,7 @@ clean_test: name: $CI_COMMIT_REF_NAME on_stop: clean_gitlab_env script: - - ansible-playbook -i $ANSIBLE_INVENTORY $DELETE_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA" + - ansible-playbook -i $ANSIBLE_INVENTORY $DELETE_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: always @@ -53,8 +53,8 @@ update_metax: stage: update environment: $CI_COMMIT_REF_NAME script: - - ansible-playbook -i $ANSIBLE_INVENTORY $UPDATE_PROXY_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA" - - ansible-playbook -i $ANSIBLE_INVENTORY $MANAGE_PLAYBOOK -e "build_id=$CI_COMMIT_SHORT_SHA" + - ansible-playbook -i $ANSIBLE_INVENTORY $UPDATE_PROXY_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" + - ansible-playbook -i $ANSIBLE_INVENTORY $MANAGE_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" rules: - if: $CI_COMMIT_BRANCH =~ /^(demo|stable|staging|test)$/ when: always @@ -64,8 +64,8 @@ clean_previous_build: stage: clean_build environment: $CI_COMMIT_REF_NAME script: - - ansible-playbook -i $ANSIBLE_INVENTORY $DELETE_PLAYBOOK -e "build_id=${CI_COMMIT_BEFORE_SHA:0:8}" + - ansible-playbook -i $ANSIBLE_INVENTORY $DELETE_PLAYBOOK -e "build_id=$CI_COMMIT_REF_SLUG repo_version=$CI_COMMIT_REF_NAME" rules: - if: $CI_COMMIT_BRANCH =~ /^(demo|stable|staging|test)$/ - when: always + when: manual - when: never diff --git a/ENV_VARS.md b/ENV_VARS.md index 7afadd56..ec31024b 100755 --- a/ENV_VARS.md +++ b/ENV_VARS.md @@ -56,6 +56,7 @@ copy .env.template to .env and fill the required values from below table. Requir | REMS_FORM_ID | no | | Required if REMS is enabled | | REMS_METAX_USER | no | | Required if REMS is enabled | | REMS_REPORTER_USER | no | | Required if REMS is enabled | +| REMS_ORGANIZATION | no | | Required if REMS is enabled | | SERVER_DOMAIN_NAME | no | metax.fd-dev.csc.fi | | ENABLE_V1_ENDPOINTS | no | True | ENABLE_V2_ENDPOINTS | no | True diff --git a/docker-compose.yml b/docker-compose.yml index 7e37522f..f4562610 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,6 +51,8 @@ services: metax-rabbitmq: image: rabbitmq:3-management hostname: metax-rabbitmq + ports: + - 8050:15672 volumes: - metax-rabbitmq:/var/lib/rabbitmq diff --git a/poetry.lock b/poetry.lock index 05e4a555..8be0ea6c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -118,23 +118,26 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "certifi" -version = "2021.5.30" +version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = "*" [[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" +name = "charset-normalizer" +version = "2.0.9" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.1" +version = "8.0.3" description = "Composable command line interface toolkit" category = "dev" optional = false @@ -187,7 +190,7 @@ tests = ["responses (>=0.10.6)", "mock (>=1.3.0)", "pytest-invenio (>=1.4.0)"] [[package]] name = "decorator" -version = "5.0.9" +version = "5.1.0" description = "Decorators for Humans" category = "dev" optional = false @@ -195,24 +198,24 @@ python-versions = ">=3.5" [[package]] name = "django" -version = "3.1.13" +version = "3.2.10" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -asgiref = ">=3.2.10,<4" +asgiref = ">=3.3.2,<4" pytz = "*" sqlparse = ">=0.2.2" [package.extras] -argon2 = ["argon2-cffi (>=16.1.0)"] +argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] [[package]] name = "django-debug-toolbar" -version = "3.2.1" +version = "3.2.3" description = "A configurable set of panels that display various debug information about the current request/response." category = "dev" optional = false @@ -230,20 +233,9 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "django-rainbowtests" -version = "0.6.0" -description = "A colorful Django Test Runner." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -django = "*" - [[package]] name = "django-split-settings" -version = "1.0.1" +version = "1.1.0" description = "Organize Django settings into multiple files and directories. Easily override and modify settings. Use wildcards and optional settings files." category = "main" optional = false @@ -262,14 +254,15 @@ django = ">=2.0" [[package]] name = "djangorestframework" -version = "3.12.4" +version = "3.13.0" description = "Web APIs for Django, made easy." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] django = ">=2.2" +pytz = "*" [[package]] name = "docutils" @@ -298,7 +291,7 @@ https = ["urllib3[secure] (>=1.24.1)"] [[package]] name = "elasticsearch" -version = "7.13.3" +version = "7.16.1" description = "Python client for Elasticsearch" category = "main" optional = false @@ -316,7 +309,7 @@ requests = ["requests (>=2.4.0,<3.0.0)"] [[package]] name = "executing" -version = "0.7.0" +version = "0.8.2" description = "Get the currently executing AST node of a frame, and other information" category = "dev" optional = false @@ -352,15 +345,15 @@ pygments = ">=2.2.0" [[package]] name = "idna" -version = "2.10" +version = "3.3" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" [[package]] name = "idutils" -version = "1.1.8" +version = "1.1.9" description = "Small library for persistent identifiers used in scholarly communication." category = "main" optional = false @@ -371,13 +364,13 @@ isbnid-fork = ">=0.4.4" six = ">=1.10" [package.extras] -all = ["Sphinx (>=1.4.2)", "check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "pydocstyle (>=1.0)", "pytest-cache (>=1.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest-runner (>=2.6.2)", "pytest (>=3.6.0)"] -docs = ["Sphinx (>=1.4.2)"] -tests = ["check-manifest (>=0.25)", "coverage (>=4.0)", "isort (>=4.2.2)", "pydocstyle (>=1.0)", "pytest-cache (>=1.0)", "pytest-cov (>=1.8.0)", "pytest-pep8 (>=1.0.6)", "pytest-runner (>=2.6.2)", "pytest (>=3.6.0)"] +all = ["Sphinx (>=3)", "pytest-cache (>=1.0)", "pytest-runner (>=2.6.2)", "pytest-invenio (>=1.4.0)"] +docs = ["Sphinx (>=3)"] +tests = ["pytest-cache (>=1.0)", "pytest-runner (>=2.6.2)", "pytest-invenio (>=1.4.0)"] [[package]] name = "imagesize" -version = "1.2.0" +version = "1.3.0" description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "main" optional = true @@ -385,7 +378,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.6.1" +version = "4.8.2" description = "Read metadata from Python packages" category = "main" optional = false @@ -398,7 +391,7 @@ zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "ipdb" @@ -415,7 +408,7 @@ toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} [[package]] name = "ipython" -version = "7.25.0" +version = "7.30.1" description = "IPython: Productive Interactive Computing" category = "dev" optional = false @@ -445,14 +438,6 @@ parallel = ["ipyparallel"] qtconsole = ["qtconsole"] test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] -[[package]] -name = "ipython-genutils" -version = "0.2.0" -description = "Vestigial utilities from IPython" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "isbnid-fork" version = "0.5.2" @@ -469,7 +454,7 @@ tests = ["pytest-pep8 (>=1.0.6)", "pytest (>=3.0.4)"] [[package]] name = "isodate" -version = "0.6.0" +version = "0.6.1" description = "An ISO 8601 date/time/duration parser and formatter" category = "main" optional = false @@ -480,7 +465,7 @@ six = "*" [[package]] name = "isort" -version = "5.9.1" +version = "5.10.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -494,7 +479,7 @@ plugins = ["setuptools"] [[package]] name = "jedi" -version = "0.18.0" +version = "0.18.1" description = "An autocompletion tool for Python that can be used for text editors." category = "dev" optional = false @@ -505,11 +490,11 @@ parso = ">=0.8.0,<0.9.0" [package.extras] qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" -version = "3.0.1" +version = "3.0.3" description = "A very fast and expressive template engine." category = "main" optional = true @@ -553,7 +538,7 @@ tornado = {version = "*", markers = "python_version > \"2.7\""} [[package]] name = "lxml" -version = "4.6.3" +version = "4.7.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -575,7 +560,7 @@ python-versions = ">=3.6" [[package]] name = "matplotlib-inline" -version = "0.1.2" +version = "0.1.3" description = "Inline Matplotlib backend for Jupyter" category = "dev" optional = false @@ -594,18 +579,18 @@ python-versions = "*" [[package]] name = "packaging" -version = "21.0" +version = "21.3" description = "Core utilities for Python packages" category = "main" optional = true python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "parso" -version = "0.8.2" +version = "0.8.3" description = "A Python Parser" category = "dev" optional = false @@ -617,11 +602,11 @@ testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "pathspec" -version = "0.8.1" +version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "pexpect" @@ -657,18 +642,18 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.19" +version = "3.0.24" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.6.2" [package.dependencies] wcwidth = "*" [[package]] name = "psycopg2-binary" -version = "2.9.1" +version = "2.9.2" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = false @@ -684,7 +669,7 @@ python-versions = "*" [[package]] name = "pygments" -version = "2.9.0" +version = "2.10.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false @@ -692,15 +677,15 @@ python-versions = ">=3.5" [[package]] name = "pyjwt" -version = "2.1.0" +version = "2.3.0" description = "JSON Web Token implementation in Python" category = "dev" optional = false python-versions = ">=3.6" [package.extras] -crypto = ["cryptography (>=3.3.1,<4.0.0)"] -dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4.0.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] +crypto = ["cryptography (>=3.3.1)"] +dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] @@ -725,11 +710,14 @@ resolved_reference = "5f6eba1201270d930ed684e15e9b9fc885649d17" [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.6" description = "Python parsing module" category = "main" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyrsistent" @@ -741,7 +729,7 @@ python-versions = ">=3.6" [[package]] name = "python-box" -version = "5.3.0" +version = "5.4.1" description = "Advanced Python dictionaries with dot notation access" category = "main" optional = false @@ -753,10 +741,11 @@ all = ["ruamel.yaml", "toml", "msgpack"] msgpack = ["msgpack"] "ruamel.yaml" = ["ruamel.yaml"] toml = ["toml"] +yaml = ["ruamel.yaml"] [[package]] name = "python-dateutil" -version = "2.8.1" +version = "2.8.2" description = "Extensions to the standard Python datetime module" category = "main" optional = false @@ -775,7 +764,7 @@ python-versions = "*" [[package]] name = "pytz" -version = "2021.1" +version = "2021.3" description = "World timezone definitions, modern and historical" category = "main" optional = false @@ -821,7 +810,7 @@ hiredis = ["hiredis (>=0.1.3)"] [[package]] name = "regex" -version = "2021.7.6" +version = "2021.11.10" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -829,25 +818,25 @@ python-versions = "*" [[package]] name = "requests" -version = "2.25.1" +version = "2.26.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "responses" -version = "0.13.3" +version = "0.13.4" description = "A utility library for mocking out the `requests` Python library." category = "dev" optional = false @@ -859,7 +848,7 @@ six = "*" urllib3 = ">=1.25.10" [package.extras] -tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] +tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "types-mock", "types-requests", "types-six", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] [[package]] name = "six" @@ -871,7 +860,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "snowballstemmer" -version = "2.1.0" +version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "main" optional = true @@ -879,7 +868,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "4.0.3" +version = "4.3.1" description = "Python documentation generator" category = "main" optional = true @@ -898,10 +887,10 @@ requests = ">=2.5.0" snowballstemmer = ">=1.1" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] @@ -1012,7 +1001,7 @@ test = ["pytest"] [[package]] name = "sqlparse" -version = "0.4.1" +version = "0.4.2" description = "A non-validating SQL parser." category = "main" optional = false @@ -1020,7 +1009,7 @@ python-versions = ">=3.5" [[package]] name = "structlog" -version = "21.1.0" +version = "21.4.0" description = "Structured Logging for Python" category = "main" optional = false @@ -1030,9 +1019,9 @@ python-versions = ">=3.6" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -dev = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-toolbox", "twisted", "pre-commit"] -docs = ["furo", "sphinx", "sphinx-toolbox", "twisted"] -tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest-randomly", "pytest (>=6.0)", "simplejson"] +dev = ["pre-commit", "rich", "cogapp", "tomli", "coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] +tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest (>=6.0)", "simplejson"] [[package]] name = "tblib" @@ -1060,37 +1049,34 @@ python-versions = ">= 3.5" [[package]] name = "traitlets" -version = "5.0.5" +version = "5.1.1" description = "Traitlets Python configuration system" category = "dev" optional = false python-versions = ">=3.7" -[package.dependencies] -ipython-genutils = "*" - [package.extras] test = ["pytest"] [[package]] name = "typed-ast" -version = "1.4.3" +version = "1.5.1" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "3.10.0.0" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.0.1" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.6" +version = "1.26.7" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1119,7 +1105,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "zipp" -version = "3.5.0" +version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1137,7 +1123,7 @@ swagger = ["PyYAML"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "f902a1619b9a3a0cdc7233bee55b16b6b3167d7ce087716503df3c9bd39fe484" +content-hash = "c0f6e79d018c633ee4c3b935d5253e27d9e155e0f2b5a33f0e9d0a67b62227a3" [metadata.files] alabaster = [ @@ -1179,16 +1165,16 @@ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] certifi = [ - {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, - {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, ] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +charset-normalizer = [ + {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, + {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, ] click = [ - {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, - {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1253,35 +1239,32 @@ datacite = [ {file = "datacite-1.1.2.tar.gz", hash = "sha256:0164bc2ff35bba643897201eb359611abb43ff5811a9ac17fb5592cd643b4443"}, ] decorator = [ - {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, - {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, + {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, + {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, ] django = [ - {file = "Django-3.1.13-py3-none-any.whl", hash = "sha256:a6e0d1ff11095b7394c079ade7094c73b2dc3df4a7a373c9b58ed73b77a97feb"}, - {file = "Django-3.1.13.tar.gz", hash = "sha256:9f8be75646f62204320b195062b1d696ba28aa3d45ee72fb7c888ffaebc5bdb2"}, + {file = "Django-3.2.10-py3-none-any.whl", hash = "sha256:df6f5eb3c797b27c096d61494507b7634526d4ce8d7c8ca1e57a4fb19c0738a3"}, + {file = "Django-3.2.10.tar.gz", hash = "sha256:074e8818b4b40acdc2369e67dcd6555d558329785408dcd25340ee98f1f1d5c4"}, ] django-debug-toolbar = [ - {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"}, - {file = "django_debug_toolbar-3.2.1-py3-none-any.whl", hash = "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"}, + {file = "django-debug-toolbar-3.2.3.tar.gz", hash = "sha256:95880677ea846ba1077d02305fd5e2b25e1da096e1d4a735b665e3340fa2ae79"}, + {file = "django_debug_toolbar-3.2.3-py3-none-any.whl", hash = "sha256:516702e1d71302bbc06059fa3c41efd1a3bd9cbb5580fc793343118d95b309e0"}, ] django-environ = [ {file = "django-environ-0.4.5.tar.gz", hash = "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde"}, {file = "django_environ-0.4.5-py2.py3-none-any.whl", hash = "sha256:c57b3c11ec1f319d9474e3e5a79134f40174b17c7cc024bbb2fad84646b120c4"}, ] -django-rainbowtests = [ - {file = "django-rainbowtests-0.6.0.tar.gz", hash = "sha256:0700ee1386935822dca296d323d67b0563cb2e5012b553ebca7c9391f2298cd9"}, -] django-split-settings = [ - {file = "django-split-settings-1.0.1.tar.gz", hash = "sha256:2da16cd967cd38315ec7ff0ae0c9db8488f8528bb2e5de26cd898328dc4bbeac"}, - {file = "django_split_settings-1.0.1-py3-none-any.whl", hash = "sha256:8d636649023289d0ef0ba08b0a4f37761adc94a29ee0ebfe65922c3cb0594ede"}, + {file = "django-split-settings-1.1.0.tar.gz", hash = "sha256:6b3aed89667a95525152026eab93a9f038ff22df6883006318b8b4a3d0ca6888"}, + {file = "django_split_settings-1.1.0-py3-none-any.whl", hash = "sha256:5d97ae64cf9ed14a831722d82ac725944667ac8c08307b7cfd22e91367b411d0"}, ] django-watchman = [ {file = "django-watchman-1.2.0.tar.gz", hash = "sha256:c38830c58984b8eb29db30a3e332968d1c7e235dee3f5c0a907d8d79a37a3125"}, {file = "django_watchman-1.2.0-py2.py3-none-any.whl", hash = "sha256:6c0b8889456ed644fcfe2f7c3294cb4ef8568a85772b7f95842e2d8c9b4bbff5"}, ] djangorestframework = [ - {file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"}, - {file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"}, + {file = "djangorestframework-3.13.0-py3-none-any.whl", hash = "sha256:48e64f08244fa0df9e2b8fbd405edec263d8e1251112a06d0073b546b7c86b9c"}, + {file = "djangorestframework-3.13.0.tar.gz", hash = "sha256:8b987d5683f5b3553dd946d4972048d3117fc526cb0bc01a3f021e81af53f39e"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, @@ -1297,14 +1280,15 @@ dulwich = [ {file = "dulwich-0.19.16.tar.gz", hash = "sha256:f74561c448bfb6f04c07de731c1181ae4280017f759b0bb04fa5770aa84ca850"}, ] elasticsearch = [ - {file = "elasticsearch-7.13.3-py2.py3-none-any.whl", hash = "sha256:76cc7670449676138acbab8872eb34867db033701310d9b01fb2ecfba5fb7234"}, - {file = "elasticsearch-7.13.3.tar.gz", hash = "sha256:d539d82552804b3d41033377b9adf11c39c749ee1764af3d74b56f42177e3281"}, + {file = "elasticsearch-7.16.1-py2.py3-none-any.whl", hash = "sha256:97e9d62db71a571c540d6dfa2a51bc2263b5c0d7b9f9896eb7116ba02373c69b"}, + {file = "elasticsearch-7.16.1.tar.gz", hash = "sha256:c024ee2e7e2509c842c4e3c5e2b99a92ceecfde06d6dac2d32a19bf566c3e175"}, ] executing = [ - {file = "executing-0.7.0-py2.py3-none-any.whl", hash = "sha256:1971c98963857f2c03f4b688d93fc4b28ce756bd102955ea8ea7ce0a7fd9a28f"}, - {file = "executing-0.7.0.tar.gz", hash = "sha256:509fe590e9da1c0659a273c42493a25af6f43d61cf36f085fc1b6cf2c6419d1f"}, + {file = "executing-0.8.2-py2.py3-none-any.whl", hash = "sha256:32fc6077b103bd19e6494a72682d66d5763cf20a106d5aa7c5ccbea4e47b0df7"}, + {file = "executing-0.8.2.tar.gz", hash = "sha256:c23bf42e9a7b9b212f185b1b2c3c91feb895963378887bb10e64a2e612ec0023"}, ] gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] icecream = [ @@ -1312,50 +1296,46 @@ icecream = [ {file = "icecream-2.1.1.tar.gz", hash = "sha256:47e00e3f4e8477996e7dc420b6fa8ba53f8ced17de65320fedb5b15997b76589"}, ] idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] idutils = [ - {file = "IDUtils-1.1.8-py2.py3-none-any.whl", hash = "sha256:6a032009da9dad262c2d1ad4978100b5937749e27862845a7bf49c38a39e3fff"}, - {file = "IDUtils-1.1.8.tar.gz", hash = "sha256:21497bf279b64aadce923a11b1ed9b601c0bf01eb82c3f952877eef026586d78"}, + {file = "IDUtils-1.1.9-py2.py3-none-any.whl", hash = "sha256:01edcde8394b73dc725958770e6e8d20088790345639982698715749f8213acc"}, + {file = "IDUtils-1.1.9.tar.gz", hash = "sha256:80c10f2ecb5cf020aa788ea860fa1f450a5a6714a0e95ae62334d5d286b395be"}, ] imagesize = [ - {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, - {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, + {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, + {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, - {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, + {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, + {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, ] ipdb = [ {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, ] ipython = [ - {file = "ipython-7.25.0-py3-none-any.whl", hash = "sha256:aa21412f2b04ad1a652e30564fff6b4de04726ce875eab222c8430edc6db383a"}, - {file = "ipython-7.25.0.tar.gz", hash = "sha256:54bbd1fe3882457aaf28ae060a5ccdef97f212a741754e420028d4ec5c2291dc"}, -] -ipython-genutils = [ - {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, - {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, + {file = "ipython-7.30.1-py3-none-any.whl", hash = "sha256:fc60ef843e0863dd4e24ab2bb5698f071031332801ecf8d1aeb4fb622056545c"}, + {file = "ipython-7.30.1.tar.gz", hash = "sha256:cb6aef731bf708a7727ab6cde8df87f0281b1427d41e65d62d4b68934fa54e97"}, ] isbnid-fork = [ {file = "isbnid_fork-0.5.2.tar.gz", hash = "sha256:8d878866aa0e7f06e700a37fce586c7398ce4837da8bca39683db7028a9c3837"}, ] isodate = [ - {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"}, - {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"}, + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, ] isort = [ - {file = "isort-5.9.1-py3-none-any.whl", hash = "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"}, - {file = "isort-5.9.1.tar.gz", hash = "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56"}, + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] jedi = [ - {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, - {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, + {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, + {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, ] jinja2 = [ - {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, - {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"}, + {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, + {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, ] jsonschema = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, @@ -1365,50 +1345,90 @@ livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] lxml = [ - {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"}, - {file = "lxml-4.6.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"}, - {file = "lxml-4.6.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"}, - {file = "lxml-4.6.3-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"}, - {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"}, - {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"}, - {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"}, - {file = "lxml-4.6.3-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"}, - {file = "lxml-4.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"}, - {file = "lxml-4.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"}, - {file = "lxml-4.6.3-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"}, - {file = "lxml-4.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"}, - {file = "lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"}, - {file = "lxml-4.6.3-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"}, - {file = "lxml-4.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"}, - {file = "lxml-4.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"}, - {file = "lxml-4.6.3-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"}, - {file = "lxml-4.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"}, - {file = "lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"}, - {file = "lxml-4.6.3-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"}, - {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"}, - {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"}, + {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, + {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, + {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"}, + {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"}, + {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"}, + {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"}, + {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"}, + {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"}, + {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"}, + {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"}, + {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"}, + {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"}, + {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"}, + {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"}, + {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"}, + {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"}, + {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"}, + {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"}, + {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"}, + {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"}, + {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"}, + {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"}, + {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"}, + {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"}, + {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"}, + {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"}, + {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"}, + {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"}, + {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"}, + {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"}, + {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, + {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1417,14 +1437,27 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1434,29 +1467,35 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] matplotlib-inline = [ - {file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"}, - {file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"}, + {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"}, + {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packaging = [ - {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, - {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] parso = [ - {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, - {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, ] pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] pexpect = [ {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, @@ -1471,56 +1510,63 @@ pika = [ {file = "pika-1.2.0.tar.gz", hash = "sha256:f023d6ac581086b124190cb3dc81dd581a149d216fa4540ac34f9be1e3970b89"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.19-py3-none-any.whl", hash = "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"}, - {file = "prompt_toolkit-3.0.19.tar.gz", hash = "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f"}, + {file = "prompt_toolkit-3.0.24-py3-none-any.whl", hash = "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506"}, + {file = "prompt_toolkit-3.0.24.tar.gz", hash = "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6"}, ] psycopg2-binary = [ - {file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d"}, - {file = "psycopg2_binary-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e"}, - {file = "psycopg2_binary-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-win32.whl", hash = "sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32"}, - {file = "psycopg2_binary-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-win32.whl", hash = "sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975"}, - {file = "psycopg2_binary-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68"}, + {file = "psycopg2-binary-2.9.2.tar.gz", hash = "sha256:234b1f48488b2f86aac04fb00cb04e5e9bcb960f34fa8a8e41b73149d581a93b"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c0e1fb7097ded2cc44d9037cfc68ad86a30341261492e7de95d180e534969fb2"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:717525cdc97b23182ff6f470fb5bf6f0bc796b5a7000c6f6699d6679991e4a5e"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3865d0cd919349c45603bd7e80249a382c5ecf8106304cfd153282adf9684b6a"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:daf6b5c62eb738872d61a1fa740d7768904911ba5a7e055ed72169d379b58beb"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:3ac83656ff4fbe7f2a956ab085e3eb1d678df54759965d509bdd6a06ce520d49"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-win32.whl", hash = "sha256:a04cfa231e7d9b63639e62166a4051cb47ca599fa341463fa3e1c48585fcee64"}, + {file = "psycopg2_binary-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:c6e16e085fe6dc6c099ee0be56657aa9ad71027465ef9591d302ba230c404c7e"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:53912199abb626a7249c662e72b70b4f57bf37f840599cec68625171435790dd"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:029e09a892b9ebc3c77851f69ce0720e1b72a9c6850460cee49b14dfbf9ccdd2"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db1b03c189f85b8df29030ad32d521dd7dcb862fd5f8892035314f5b886e70ce"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:2eecbdc5fa5886f2dd6cc673ce4291cc0fb8900965315268960ad9c2477f8276"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:a77e98c68b0e6c51d4d6a994d22b30e77276cbd33e4aabdde03b9ad3a2c148aa"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:bf31e6fdb4ec1f6d98a07f48836508ed6edd19b48b13bbf168fbc1bd014b4ca2"}, + {file = "psycopg2_binary-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f9c37ecb173d76cf49e519133fd70851b8f9c38b6b8c1cb7fcfc71368d4cc6fc"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:a507db7758953b1b170c4310691a1a89877029b1e11b08ba5fc8ae3ddb35596b"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e4bbcfb403221ea1953f3e0a85cef00ed15c1683a66cf35c956a7e37c33a4c4"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4dff0f15af6936c6fe6da7067b4216edbbe076ad8625da819cc066591b1133c"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:8d2aafe46eb87742425ece38130510fbb035787ee89a329af299029c4d9ae318"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:37c8f00f7a2860bac9f7a54f03c243fc1dd9b367e5b2b52f5a02e5f4e9d8c49b"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:ef97578fab5115e3af4334dd3376dea3c3a79328a3314b21ec7ced02920b916d"}, + {file = "psycopg2_binary-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7e6bd4f532c2cd297b81114526176b240109a1c52020adca69c3f3226c65dc18"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:eeee7b18c51d02e49bf1984d7af26e8843fe68e31fa1cbab5366ebdfa1c89ade"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:497372cc76e6cbce2f51b37be141f360a321423c03eb9be45524b1d123f4cd11"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671699aff57d22a245b7f4bba89e3de97dc841c5e98bd7f685429b2b20eca47"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:b9d45374ba98c1184df9cce93a0b766097544f8bdfcd5de83ff10f939c193125"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:a1852c5bef7e5f52bd43fde5eda610d4df0fb2efc31028150933e84b4140d47a"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-win32.whl", hash = "sha256:108b0380969ddab7c8ef2a813a57f87b308b2f88ec15f1a1e7b653964a3cfb25"}, + {file = "psycopg2_binary-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:14427437117f38e65f71db65d8eafd0e86837be456567798712b8da89db2b2dd"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:578c279cd1ce04f05ae0912530ece00bab92854911808e5aec27588aba87e361"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2dea4deac3dd3687e32daeb0712ee96c535970dfdded37a11de6a21145ab0e"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b592f09ff18cfcc9037b9a976fcd62db48cae9dbd5385f2471d4c2ba40c52b4d"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:3a320e7a804f3886a599fea507364aaafbb8387027fffcdfbd34d96316c806c7"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7585ca73dcfe326f31fafa8f96e6bb98ea9e9e46c7a1924ec8101d797914ae27"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-win32.whl", hash = "sha256:9c0aaad07941419926b9bd00171e49fe6b06e42e5527fb91671e137fe6c93d77"}, + {file = "psycopg2_binary-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa2847d8073951dbc84c4f8b32c620764db3c2eb0d99a04835fecfab7d04816e"}, ] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] pygments = [ - {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, - {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] pyjwt = [ - {file = "PyJWT-2.1.0-py3-none-any.whl", hash = "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1"}, - {file = "PyJWT-2.1.0.tar.gz", hash = "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"}, + {file = "PyJWT-2.3.0-py3-none-any.whl", hash = "sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"}, + {file = "PyJWT-2.3.0.tar.gz", hash = "sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41"}, ] pyoai = [] pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] pyrsistent = [ {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, @@ -1546,12 +1592,12 @@ pyrsistent = [ {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, ] python-box = [ - {file = "python-box-5.3.0.tar.gz", hash = "sha256:4ed4ef5d34de505a65c01e3f1911de8cdb29484fcae0c035141dce535c6c194a"}, - {file = "python_box-5.3.0-py3-none-any.whl", hash = "sha256:f2a531f9f5bbef078c175fad6abb31e9b59d40d121ea79993197e6bb221c6be6"}, + {file = "python-box-5.4.1.tar.gz", hash = "sha256:b68e0f8abc86f3deda751b3390f64df64a0989459de51ba4db949662a7b4d8ac"}, + {file = "python_box-5.4.1-py3-none-any.whl", hash = "sha256:60ae9156de34cf92b899bd099580950df70a5b0813e67a3310a1cdd1976457fa"}, ] python-dateutil = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-simplexquery = [ {file = "python-simplexquery-1.0.5.3.win32-py2.6.exe", hash = "sha256:a32281dd8a923930c177c7a5b6124e4f358c02773e40e6e734a1217fb9a5ab86"}, @@ -1561,8 +1607,8 @@ python-simplexquery = [ {file = "python-simplexquery-1.0.5.3.zip", hash = "sha256:4849070678538d26778c9902c58eac13a88beaffc526e69ee5e3db3744499a2b"}, ] pytz = [ - {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, - {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, ] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, @@ -1604,67 +1650,100 @@ redis = [ {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] regex = [ - {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, - {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, - {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, - {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, - {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, - {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, - {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, - {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, - {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, - {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, - {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, - {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, - {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, + {file = "regex-2021.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf"}, + {file = "regex-2021.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0"}, + {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4"}, + {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9ed0b1e5e0759d6b7f8e2f143894b2a7f3edd313f38cf44e1e15d360e11749b"}, + {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:473e67837f786404570eae33c3b64a4b9635ae9f00145250851a1292f484c063"}, + {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2fee3ed82a011184807d2127f1733b4f6b2ff6ec7151d83ef3477f3b96a13d03"}, + {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d5fd67df77bab0d3f4ea1d7afca9ef15c2ee35dfb348c7b57ffb9782a6e4db6e"}, + {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5d408a642a5484b9b4d11dea15a489ea0928c7e410c7525cd892f4d04f2f617b"}, + {file = "regex-2021.11.10-cp310-cp310-win32.whl", hash = "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a"}, + {file = "regex-2021.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12"}, + {file = "regex-2021.11.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23"}, + {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e"}, + {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:74cbeac0451f27d4f50e6e8a8f3a52ca074b5e2da9f7b505c4201a57a8ed6286"}, + {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:3598893bde43091ee5ca0a6ad20f08a0435e93a69255eeb5f81b85e81e329264"}, + {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:50a7ddf3d131dc5633dccdb51417e2d1910d25cbcf842115a3a5893509140a3a"}, + {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:61600a7ca4bcf78a96a68a27c2ae9389763b5b94b63943d5158f2a377e09d29a"}, + {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:563d5f9354e15e048465061509403f68424fef37d5add3064038c2511c8f5e00"}, + {file = "regex-2021.11.10-cp36-cp36m-win32.whl", hash = "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4"}, + {file = "regex-2021.11.10-cp36-cp36m-win_amd64.whl", hash = "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e"}, + {file = "regex-2021.11.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e"}, + {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f"}, + {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:42b50fa6666b0d50c30a990527127334d6b96dd969011e843e726a64011485da"}, + {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6e1d2cc79e8dae442b3fa4a26c5794428b98f81389af90623ffcc650ce9f6732"}, + {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:0416f7399e918c4b0e074a0f66e5191077ee2ca32a0f99d4c187a62beb47aa05"}, + {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ce298e3d0c65bd03fa65ffcc6db0e2b578e8f626d468db64fdf8457731052942"}, + {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dc07f021ee80510f3cd3af2cad5b6a3b3a10b057521d9e6aaeb621730d320c5a"}, + {file = "regex-2021.11.10-cp37-cp37m-win32.whl", hash = "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec"}, + {file = "regex-2021.11.10-cp37-cp37m-win_amd64.whl", hash = "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4"}, + {file = "regex-2021.11.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83"}, + {file = "regex-2021.11.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe"}, + {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94"}, + {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f5be7805e53dafe94d295399cfbe5227f39995a997f4fd8539bf3cbdc8f47ca8"}, + {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a955b747d620a50408b7fdf948e04359d6e762ff8a85f5775d907ceced715129"}, + {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:139a23d1f5d30db2cc6c7fd9c6d6497872a672db22c4ae1910be22d4f4b2068a"}, + {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ca49e1ab99593438b204e00f3970e7a5f70d045267051dfa6b5f4304fcfa1dbf"}, + {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:96fc32c16ea6d60d3ca7f63397bff5c75c5a562f7db6dec7d412f7c4d2e78ec0"}, + {file = "regex-2021.11.10-cp38-cp38-win32.whl", hash = "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc"}, + {file = "regex-2021.11.10-cp38-cp38-win_amd64.whl", hash = "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d"}, + {file = "regex-2021.11.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b"}, + {file = "regex-2021.11.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b"}, + {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef"}, + {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cd410a1cbb2d297c67d8521759ab2ee3f1d66206d2e4328502a487589a2cb21b"}, + {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e6096b0688e6e14af6a1b10eaad86b4ff17935c49aa774eac7c95a57a4e8c296"}, + {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:529801a0d58809b60b3531ee804d3e3be4b412c94b5d267daa3de7fadef00f49"}, + {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f594b96fe2e0821d026365f72ac7b4f0b487487fb3d4aaf10dd9d97d88a9737"}, + {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2409b5c9cef7054dde93a9803156b411b677affc84fca69e908b1cb2c540025d"}, + {file = "regex-2021.11.10-cp39-cp39-win32.whl", hash = "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a"}, + {file = "regex-2021.11.10-cp39-cp39-win_amd64.whl", hash = "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29"}, + {file = "regex-2021.11.10.tar.gz", hash = "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6"}, ] requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] responses = [ - {file = "responses-0.13.3-py2.py3-none-any.whl", hash = "sha256:b54067596f331786f5ed094ff21e8d79e6a1c68ef625180a7d34808d6f36c11b"}, - {file = "responses-0.13.3.tar.gz", hash = "sha256:18a5b88eb24143adbf2b4100f328a2f5bfa72fbdacf12d97d41f07c26c45553d"}, + {file = "responses-0.13.4-py2.py3-none-any.whl", hash = "sha256:d8d0f655710c46fd3513b9202a7f0dcedd02ca0f8cf4976f27fa8ab5b81e656d"}, + {file = "responses-0.13.4.tar.gz", hash = "sha256:9476775d856d3c24ae660bbebe29fb6d789d4ad16acd723efbfb6ee20990b899"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] snowballstemmer = [ - {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, - {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] sphinx = [ - {file = "Sphinx-4.0.3-py3-none-any.whl", hash = "sha256:5747f3c855028076fcff1e4df5e75e07c836f0ac11f7df886747231092cfe4ad"}, - {file = "Sphinx-4.0.3.tar.gz", hash = "sha256:dff357e6a208eb7edb2002714733ac21a9fe597e73609ff417ab8cf0c6b4fbb8"}, + {file = "Sphinx-4.3.1-py3-none-any.whl", hash = "sha256:048dac56039a5713f47a554589dc98a442b39226a2b9ed7f82797fcb2fe9253f"}, + {file = "Sphinx-4.3.1.tar.gz", hash = "sha256:32a5b3e9a1b176cc25ed048557d4d3d01af635e6b76c5bc7a43b0a34447fbd45"}, ] sphinx-autobuild = [ {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, @@ -1699,12 +1778,12 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] sqlparse = [ - {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, - {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, + {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, + {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, ] structlog = [ - {file = "structlog-21.1.0-py2.py3-none-any.whl", hash = "sha256:62f06fc0ee32fb8580f0715eea66cb87271eb7efb0eaf9af6b639cba8981de47"}, - {file = "structlog-21.1.0.tar.gz", hash = "sha256:d9d2d890532e8db83c6977a2a676fb1889922ff0c26ad4dc0ecac26f9fafbc57"}, + {file = "structlog-21.4.0-py3-none-any.whl", hash = "sha256:6ed8fadb27cf8362be0e606f5e79ccdd3b1e879aac65f9dc0ac3033fd013a7be"}, + {file = "structlog-21.4.0.tar.gz", hash = "sha256:305a66201f9605a2e8a2595271a446f258175901c09c01e4c2c2a8ac5b68edf1"}, ] tblib = [ {file = "tblib-1.7.0-py2.py3-none-any.whl", hash = "sha256:289fa7359e580950e7d9743eab36b0691f0310fce64dee7d9c31065b8f723e23"}, @@ -1758,49 +1837,37 @@ tornado = [ {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, ] traitlets = [ - {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, - {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, + {file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"}, + {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"}, ] typed-ast = [ - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, - {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, - {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, - {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, - {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, - {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, - {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, - {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, - {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, - {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, + {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"}, + {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"}, + {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"}, + {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"}, + {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"}, + {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"}, + {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"}, + {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"}, + {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"}, + {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"}, + {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"}, + {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"}, + {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"}, + {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"}, + {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"}, + {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"}, + {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ - {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, - {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, @@ -1811,6 +1878,6 @@ xmltodict = [ {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, ] zipp = [ - {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, - {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, ] diff --git a/pyproject.toml b/pyproject.toml index 8742b747..7a0af20b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Your Name "] [tool.poetry.dependencies] python = "^3.7" -Django = "<3.2" +Django = "<3.3" datacite = "^1.0.1" djangorestframework = "^3.12.4" gunicorn = "^20.1.0" @@ -37,7 +37,6 @@ django-watchman = "^1.2.0" icecream = "^2.1.0" black = {version = "^20.8b1", allow-prereleases = true} tblib = "^1.7.0" -django-rainbowtests = "^0.6.0" django-debug-toolbar = "^3.2" PyJWT = "^2.0.1" ipdb = "^0.13.7" diff --git a/requirements.txt b/requirements.txt index f7f3a93c..9c5f41ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,88 +8,86 @@ autosemver==0.5.5 babel==2.9.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" backcall==0.2.0; python_version >= "3.7" black==20.8b1; python_version >= "3.6" -certifi==2021.5.30; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" -chardet==4.0.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -click==8.0.1; python_version >= "3.6" +certifi==2021.10.8; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.6" +charset-normalizer==2.0.9; python_full_version >= "3.6.0" and python_version >= "3.6" +click==8.0.3; python_version >= "3.6" colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" and platform_system == "Windows" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0" and platform_system == "Windows" coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") datacite==1.1.2 -decorator==5.0.9; python_version >= "3.7" -django-debug-toolbar==3.2.1; python_version >= "3.6" +decorator==5.1.0; python_version >= "3.7" +django-debug-toolbar==3.2.3; python_version >= "3.6" django-environ==0.4.5 -django-rainbowtests==0.6.0 -django-split-settings==1.0.1; python_version >= "3.6" and python_version < "4.0" +django-split-settings==1.1.0; python_version >= "3.6" and python_version < "4.0" django-watchman==1.2.0 -django==3.1.13; python_version >= "3.6" -djangorestframework==3.12.4; python_version >= "3.5" +django==3.2.10; python_version >= "3.6" +djangorestframework==3.13.0; python_version >= "3.6" docutils==0.16; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" dulwich==0.19.16 -elasticsearch==7.13.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0" and python_version < "4") -executing==0.7.0 +elasticsearch==7.16.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0" and python_version < "4") +executing==0.8.2 gunicorn==20.1.0; python_version >= "3.5" icecream==2.1.1 -idna==2.10; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -idutils==1.1.8 -imagesize==1.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -importlib-metadata==4.6.1; python_version >= "3.6" and python_version < "3.8" +idna==3.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" +idutils==1.1.9 +imagesize==1.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +importlib-metadata==4.8.2; python_version >= "3.6" and python_version < "3.8" ipdb==0.13.9; python_version >= "2.7" -ipython-genutils==0.2.0; python_version >= "3.7" -ipython==7.25.0; python_version >= "3.7" +ipython==7.30.1; python_version >= "3.7" isbnid-fork==0.5.2 -isodate==0.6.0 -isort==5.9.1; python_full_version >= "3.6.1" and python_version < "4.0" -jedi==0.18.0; python_version >= "3.7" -jinja2==3.0.1; python_version >= "3.6" +isodate==0.6.1 +isort==5.10.1; python_full_version >= "3.6.1" and python_version < "4.0" +jedi==0.18.1; python_version >= "3.7" +jinja2==3.0.3; python_version >= "3.6" jsonschema==3.2.0 livereload==2.6.3; python_version >= "3.6" -lxml==4.6.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +lxml==4.7.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") markupsafe==2.0.1; python_version >= "3.6" -matplotlib-inline==0.1.2; python_version >= "3.7" +matplotlib-inline==0.1.3; python_version >= "3.7" mypy-extensions==0.4.3; python_version >= "3.6" -packaging==21.0; python_version >= "3.6" -parso==0.8.2; python_version >= "3.7" -pathspec==0.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +packaging==21.3; python_version >= "3.6" +parso==0.8.3; python_version >= "3.7" +pathspec==0.9.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.7" pickleshare==0.7.5; python_version >= "3.7" pika==1.2.0 -prompt-toolkit==3.0.19; python_full_version >= "3.6.1" and python_version >= "3.7" -psycopg2-binary==2.9.1; python_version >= "3.6" +prompt-toolkit==3.0.24; python_full_version >= "3.6.2" and python_version >= "3.7" +psycopg2-binary==2.9.2; python_version >= "3.6" ptyprocess==0.7.0; sys_platform != "win32" and python_version >= "3.7" -pygments==2.9.0; python_version >= "3.7" -pyjwt==2.1.0; python_version >= "3.6" +pygments==2.10.0; python_version >= "3.7" +pyjwt==2.3.0; python_version >= "3.6" pyoai @ git+https://github.com/infrae/pyoai@5f6eba12 -pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" +pyparsing==3.0.6; python_version >= "3.6" pyrsistent==0.18.0; python_version >= "3.6" -python-box==5.3.0; python_version >= "3.6" -python-dateutil==2.8.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") +python-box==5.4.1; python_version >= "3.6" +python-dateutil==2.8.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") python-simplexquery==1.0.5.3 -pytz==2021.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +pytz==2021.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" pyyaml==5.4.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") rdflib==5.0.0 redis==3.5.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -regex==2021.7.6; python_version >= "3.6" -requests==2.25.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" -responses==0.13.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +regex==2021.11.10; python_version >= "3.6" +requests==2.26.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" +responses==0.13.4; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") six==1.16.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" -snowballstemmer==2.1.0; python_version >= "3.6" +snowballstemmer==2.2.0; python_version >= "3.6" sphinx-autobuild==2021.3.14; python_version >= "3.6" sphinx-rtd-theme==0.5.2 -sphinx==4.0.3; python_version >= "3.6" +sphinx==4.3.1; python_version >= "3.6" sphinxcontrib-applehelp==1.0.2; python_version >= "3.6" sphinxcontrib-devhelp==1.0.2; python_version >= "3.6" sphinxcontrib-htmlhelp==2.0.0; python_version >= "3.6" sphinxcontrib-jsmath==1.0.1; python_version >= "3.6" sphinxcontrib-qthelp==1.0.3; python_version >= "3.6" sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.6" -sqlparse==0.4.1; python_version >= "3.6" -structlog==21.1.0; python_version >= "3.6" +sqlparse==0.4.2; python_version >= "3.6" +structlog==21.4.0; python_version >= "3.6" tblib==1.7.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") toml==0.10.2; python_version > "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version > "3.6" and python_version < "4" tornado==6.1; python_version >= "3.6" -traitlets==5.0.5; python_version >= "3.7" -typed-ast==1.4.3; python_version >= "3.6" -typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6" -urllib3==1.26.6; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" -wcwidth==0.2.5; python_full_version >= "3.6.1" and python_version >= "3.7" +traitlets==5.1.1; python_version >= "3.7" +typed-ast==1.5.1; python_version >= "3.6" +typing-extensions==4.0.1; python_version < "3.8" and python_version >= "3.6" +urllib3==1.26.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.6" +wcwidth==0.2.5; python_full_version >= "3.6.2" and python_version >= "3.7" xmltodict==0.12.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") -zipp==3.5.0; python_version >= "3.6" and python_version < "3.8" +zipp==3.6.0; python_version >= "3.6" and python_version < "3.8" diff --git a/src/metax_api/api/rest/base/router.py b/src/metax_api/api/rest/base/router.py index 742103a3..ac745be9 100755 --- a/src/metax_api/api/rest/base/router.py +++ b/src/metax_api/api/rest/base/router.py @@ -27,6 +27,7 @@ DataCatalogViewSet, DatasetViewSet, DirectoryViewSet, + EditorPermissionViewSet, FileStorageViewSet, FileViewSet, SchemaViewSet, @@ -83,4 +84,9 @@ def get_default_basename(self, viewset): DatasetViewSet, ) +router.register( + "datasets/(?P.+)/editor_permissions/users", + EditorPermissionViewSet, +) + api_urlpatterns = router.urls diff --git a/src/metax_api/api/rest/base/serializers/__init__.py b/src/metax_api/api/rest/base/serializers/__init__.py index 1c243720..84d9c24b 100755 --- a/src/metax_api/api/rest/base/serializers/__init__.py +++ b/src/metax_api/api/rest/base/serializers/__init__.py @@ -13,3 +13,4 @@ from .file_storage_serializer import FileStorageSerializer from .serializer_utils import validate_json from .xml_metadata_serializer import XmlMetadataSerializer +from .editor_permissions_serializer import EditorPermissionsSerializer diff --git a/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py b/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py index bd6294ad..0b8dcfab 100755 --- a/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py +++ b/src/metax_api/api/rest/base/serializers/data_catalog_serializer.py @@ -28,6 +28,8 @@ class Meta: "catalog_record_services_create", "catalog_record_group_read", "catalog_record_services_read", + "publish_to_ttv", + "publish_to_etsin", ) + CommonSerializer.Meta.fields extra_kwargs = CommonSerializer.Meta.extra_kwargs diff --git a/src/metax_api/api/rest/base/serializers/editor_permissions_serializer.py b/src/metax_api/api/rest/base/serializers/editor_permissions_serializer.py new file mode 100644 index 00000000..b306b416 --- /dev/null +++ b/src/metax_api/api/rest/base/serializers/editor_permissions_serializer.py @@ -0,0 +1,20 @@ +from django.core.validators import EMPTY_VALUES + +from rest_framework.serializers import ValidationError, ModelSerializer + +from metax_api.models import EditorUserPermission + +from .common_serializer import CommonSerializer + + +class EditorPermissionsSerializer(ModelSerializer): + class Meta: + model = EditorUserPermission + fields = "__all__" + + extra_kwargs = CommonSerializer.Meta.extra_kwargs + + def validate(self, attrs): + data = ModelSerializer.validate(self, attrs) + + return data diff --git a/src/metax_api/api/rest/base/views/__init__.py b/src/metax_api/api/rest/base/views/__init__.py index d5c62dd9..027ae76f 100755 --- a/src/metax_api/api/rest/base/views/__init__.py +++ b/src/metax_api/api/rest/base/views/__init__.py @@ -10,6 +10,7 @@ from .data_catalog_view import DataCatalogViewSet from .dataset_view import DatasetViewSet from .directory_view import DirectoryViewSet +from .editor_permissions_view import EditorPermissionViewSet from .file_storage_view import FileStorageViewSet from .file_view import FileViewSet from .schema_view import SchemaViewSet diff --git a/src/metax_api/api/rest/base/views/common_view.py b/src/metax_api/api/rest/base/views/common_view.py index f5f0be75..053a5a40 100755 --- a/src/metax_api/api/rest/base/views/common_view.py +++ b/src/metax_api/api/rest/base/views/common_view.py @@ -176,11 +176,11 @@ def get_queryset(self): q_filters = [] deduplicated_q_filters = [] - CS.set_if_modified_since_filter(self.request, additional_filters) - if hasattr(self, "queryset_search_params"): additional_filters.update(**self.queryset_search_params) + CS.set_if_modified_since_filter(self.request, additional_filters) + if "q_filters" in additional_filters: # Q-filter objects, which can contain more complex filter options such as OR-clauses q_filters = additional_filters.pop("q_filters") @@ -377,7 +377,9 @@ def _check_and_store_bulk_error(self, request, response): """ if "failed" in response.data and len(response.data["failed"]): try: - error_json = ApiErrorSerializerV2.request_to_json(self.request, response, other={"bulk_request": True}) + error_json = ApiErrorSerializerV2.request_to_json( + self.request, response, other={"bulk_request": True} + ) response.data["error_identifier"] = error_json["identifier"] if settings.ENABLE_API_ERROR_OBJECTS: rabbitmq.publish(error_json, exchange="apierrors") diff --git a/src/metax_api/api/rest/base/views/dataset_view.py b/src/metax_api/api/rest/base/views/dataset_view.py index bd416afa..648feb45 100755 --- a/src/metax_api/api/rest/base/views/dataset_view.py +++ b/src/metax_api/api/rest/base/views/dataset_view.py @@ -10,6 +10,7 @@ from django.conf import settings from django.http import Http404 + from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response @@ -270,7 +271,7 @@ def _search_using_dataset_identifiers(self): @action(detail=True, methods=["get"], url_path="redis") def redis_test(self, request, pk=None): # pragma: no cover if request.user.username != "metax": - raise Http403() + raise Http403({"detail": ["Access denied."]}) try: cached = self.cache.get("cr-1211%s" % pk) except: @@ -295,7 +296,7 @@ def redis_test(self, request, pk=None): # pragma: no cover @action(detail=True, methods=["get"], url_path="rabbitmq") def rabbitmq_test(self, request, pk=None): # pragma: no cover if request.user.username != "metax": - raise Http403() + raise Http403({"detail": ["Access denied."]}) rabbitmq.publish({"msg": "hello create"}, routing_key="create", exchange="datasets") rabbitmq.publish({"msg": "hello update"}, routing_key="update", exchange="datasets") return Response(data={}, status=status.HTTP_200_OK) @@ -311,7 +312,7 @@ def update_cr_total_files_byte_sizes(self, request): :return: """ if request.user.username != "metax": - raise Http403() + raise Http403({"detail": ["Access denied."]}) # Get all IDs for ida data catalogs ida_catalog_ids = [] for dc in DataCatalog.objects.filter( @@ -340,7 +341,7 @@ def update_cr_directory_browsing_data(self, request): :return: """ if request.user.username != "metax": - raise Http403() + raise Http403({"detail": ["Access denied."]}) if "id" in request.query_params: # in order to update one record only, use query param ?id=integer. useful for testcases @@ -389,7 +390,7 @@ def flush_password(self, request): # pragma: no cover with open("/home/metax-user/flush_password", "w") as f: dump(request.data, f) else: - raise Http403 + raise Http403({"detail": ["Access denied."]}) _logger.debug("FLUSH password set") return Response(data=None, status=status.HTTP_204_NO_CONTENT) @@ -421,4 +422,4 @@ def flush_records(self, request): # pragma: no cover _logger.debug("FLUSH called by %s" % request.user.username) return Response(data=None, status=status.HTTP_204_NO_CONTENT) - return Response(data=None, status=status.HTTP_403_FORBIDDEN) + return Response({"error": "Access denied"}, status=status.HTTP_403_FORBIDDEN) diff --git a/src/metax_api/api/rest/base/views/directory_view.py b/src/metax_api/api/rest/base/views/directory_view.py index 587363c2..1f45461e 100755 --- a/src/metax_api/api/rest/base/views/directory_view.py +++ b/src/metax_api/api/rest/base/views/directory_view.py @@ -163,7 +163,7 @@ def update_byte_sizes_and_file_counts(self, request): # pragma: no cover correct mistakes in real data. """ if request.user.username != "metax": - raise Http403 + raise Http403({"detail": ["Access denied."]}) for p in ( Directory.objects.all() diff --git a/src/metax_api/api/rest/base/views/editor_permissions_view.py b/src/metax_api/api/rest/base/views/editor_permissions_view.py new file mode 100644 index 00000000..2c87c001 --- /dev/null +++ b/src/metax_api/api/rest/base/views/editor_permissions_view.py @@ -0,0 +1,144 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT +import datetime +import logging + +from django.core.validators import EMPTY_VALUES + +from django.shortcuts import get_object_or_404 +from rest_framework import status +from rest_framework.response import Response + +from metax_api.exceptions import Http403 +from metax_api.models import CatalogRecord +from metax_api.models.catalog_record import PermissionRole, EditorUserPermission +from metax_api.permissions import ServicePermissions, EndUserPermissions +from metax_api.services import CommonService + +from ..serializers import EditorPermissionsSerializer +from .common_view import CommonViewSet + +_logger = logging.getLogger(__name__) + + +class EditorPermissionViewSet(CommonViewSet): + lookup_field = "user_id" + permission_classes = [ + ServicePermissions, + EndUserPermissions, + ] + serializer_class = EditorPermissionsSerializer + + def __init__(self, *args, **kwargs): + super(EditorPermissionViewSet, self).__init__(*args, **kwargs) + + def _user_has_access_to_permissions(self, cr): + """ + Allow only services and dataset owner access permissions. + """ + if self.request.user.is_service: + if self.request.method == "GET": + return True + # for updating perms, require edit access to catalog records + return cr._check_catalog_permissions( + cr.data_catalog.catalog_record_group_edit, + cr.data_catalog.catalog_record_services_edit, + self.request, + ) + if cr.user_is_owner(self.request): + return True + # unknown user + return False + + def get_queryset(self): + if CommonService.is_primary_key(self.kwargs["cr_identifier"]): + cr = get_object_or_404(CatalogRecord, pk=int(self.kwargs["cr_identifier"])) + else: + cr = get_object_or_404(CatalogRecord, identifier=self.kwargs["cr_identifier"]) + if not self._user_has_access_to_permissions(cr): + raise Http403({"detail": ["You do not have access to permissions of this dataset."]}) + return cr.editor_permissions.users + + def list(self, request, *args, **kwargs): + users = self.get_queryset() + editorserializer = EditorPermissionsSerializer(users.all(), many=True) + return Response(editorserializer.data) + + def create(self, request, *args, **kwargs): + data = request.data + + perms_id = self.get_queryset().instance.id + if "user_id" not in data: + return Response({"user_id": "Missing user_id"}, status=status.HTTP_400_BAD_REQUEST) + + editorserializer = None + try: + removed_user = EditorUserPermission.objects_unfiltered.get( + user_id=data.get("user_id"), editor_permissions_id=perms_id + ) + except EditorUserPermission.DoesNotExist: + removed_user = None + + if removed_user not in EMPTY_VALUES and removed_user.removed is True: + data['date_modified'] = datetime.datetime.now() + data['date_removed'] = None + data['removed'] = False + editorserializer = EditorPermissionsSerializer(removed_user, data=data, partial=True) + elif removed_user not in EMPTY_VALUES and removed_user.removed is False: + return Response( + {'user_id': "User_id already exists"}, status=status.HTTP_400_BAD_REQUEST + ) + else: + data['editor_permissions'] = perms_id + data['date_created'] = datetime.datetime.now() + editorserializer = EditorPermissionsSerializer(data=data) + if editorserializer.is_valid(): + editorserializer.save() + return Response(editorserializer.data, status=status.HTTP_201_CREATED) + else: + return Response(editorserializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, *args, **kwargs): + user = self.get_object() + users = self.get_queryset() + + creators = users.filter(role=PermissionRole.CREATOR, removed=False).count() + if user.role == PermissionRole.CREATOR and creators < 2: + return Response( + {"error": "Can't delete last creator"}, status=status.HTTP_400_BAD_REQUEST + ) + else: + user.remove() + return Response(status=status.HTTP_200_OK) + + def partial_update(self, request, **kwargs): + data = request.data + user = self.get_object() + users = self.get_queryset() + + if 'role' in data and user.role == PermissionRole.CREATOR: + creators = users.filter(role=PermissionRole.CREATOR, removed=False).count() + if creators < 2 and data.get('role') != PermissionRole.CREATOR: + return Response({"error": "Can't change last creator"}, status=status.HTTP_400_BAD_REQUEST) + + data['date_modified'] = datetime.datetime.now() + serializer = EditorPermissionsSerializer(user, data=data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def update_bulk(self, request, *args, **kwargs): + return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) + + def partial_update_bulk(self, request, *args, **kwargs): + return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) + + def destroy_bulk(self, request, *args, **kwargs): + return Response({}, status=status.HTTP_405_METHOD_NOT_ALLOWED) + diff --git a/src/metax_api/api/rest/v2/router.py b/src/metax_api/api/rest/v2/router.py index 46e703d5..10bcd0c9 100755 --- a/src/metax_api/api/rest/v2/router.py +++ b/src/metax_api/api/rest/v2/router.py @@ -29,6 +29,7 @@ FileStorageViewSet, FileViewSet, SchemaViewSet, + EditorPermissionViewSet, ) from metax_api.api.rest.v2.views import ApiErrorViewSetV2 @@ -110,4 +111,9 @@ def __init__(self, *args, **kwargs): DatasetViewSet, ) +router_v2.register( + "datasets/(?P.+)/editor_permissions/users", + EditorPermissionViewSet, +) + api_urlpatterns = router_v1.urls + router_v2.urls diff --git a/src/metax_api/api/rest/v2/views/api_error_view.py b/src/metax_api/api/rest/v2/views/api_error_view.py index c0e477d4..ac926d70 100644 --- a/src/metax_api/api/rest/v2/views/api_error_view.py +++ b/src/metax_api/api/rest/v2/views/api_error_view.py @@ -29,7 +29,7 @@ class ApiErrorViewSetV2(CommonViewSet): def initial(self, request, *args, **kwargs): if request.user.username != "metax": - raise Http403 + raise Http403({"detail": ["Access denied."]}) return super().initial(request, *args, **kwargs) @action(detail=False, methods=["post"], url_path="flush") diff --git a/src/metax_api/api/rest/v2/views/dataset_view.py b/src/metax_api/api/rest/v2/views/dataset_view.py index 84eb1776..29e361e1 100755 --- a/src/metax_api/api/rest/v2/views/dataset_view.py +++ b/src/metax_api/api/rest/v2/views/dataset_view.py @@ -41,7 +41,7 @@ def projects_list(self, request, pk=None): cr = self.get_object() if not cr.user_is_privileged(request): - raise Http403 + raise Http403({"detail": ["Access denied."]}) projects = [ p diff --git a/src/metax_api/api/rpc/base/views/dataset_rpc.py b/src/metax_api/api/rpc/base/views/dataset_rpc.py index 9a8f2336..df35d13e 100755 --- a/src/metax_api/api/rpc/base/views/dataset_rpc.py +++ b/src/metax_api/api/rpc/base/views/dataset_rpc.py @@ -9,6 +9,7 @@ from json import load from django.conf import settings as django_settings +from django.db import connection from django.http import Http404 from rest_framework import status from rest_framework.decorators import action @@ -198,3 +199,44 @@ def _save_and_publish_dataset(self, cr, action): super(CatalogRecord, cr).save(update_fields=["preservation_identifier"]) DataciteDOIUpdate(cr, cr.preservation_identifier, action)() + + @action(detail=False, methods=["post"], url_path="flush_user_data") + def flush_user_data(self, request): + """ + Permanently delete datasets created by a certain user. + + WARNING! Not meant for active production use!! + """ + if django_settings.ENV == "production": + raise Http400({"detail": ["API currently allowed only in test environments"]}) + + if "metadata_provider_user" not in request.query_params: + raise Http400({"detail": ["metadata_provider_user is a required query parameter"]}) + + user = request.query_params["metadata_provider_user"] + + sql_delete_cr_files = """ + delete from metax_api_catalogrecord_files + where catalogrecord_id in ( + select id from metax_api_catalogrecord where metadata_provider_user = %s + ) + """ + + sql_delete_datasets = """ + delete from metax_api_catalogrecord where metadata_provider_user = %s + """ + + _logger.info( + "Flushing datasets for user %s on the request of user: %s" % (user, request.user.username) + ) + + with connection.cursor() as cr: + cr.execute(sql_delete_cr_files, [user]) + cr.execute(sql_delete_datasets, [user]) + if cr.rowcount == 0: + _logger.info("No datasets found for user %s" % user) + return Response(user, status=status.HTTP_404_NOT_FOUND) + + _logger.info("Permanently deleted all datasets created by user %s" % user) + + return Response(data=None, status=status.HTTP_204_NO_CONTENT) diff --git a/src/metax_api/api/rpc/base/views/file_rpc.py b/src/metax_api/api/rpc/base/views/file_rpc.py index eb8dd169..e4567b98 100755 --- a/src/metax_api/api/rpc/base/views/file_rpc.py +++ b/src/metax_api/api/rpc/base/views/file_rpc.py @@ -33,7 +33,7 @@ def delete_project(self, request): return FileService.delete_project(request.query_params["project_identifier"]) @action(detail=False, methods=["post"], url_path="flush_project") - def flush_project(self, request): # pragma: no cover + def flush_project(self, request): """ Permanently delete an entire project's files and directories. @@ -70,6 +70,9 @@ def flush_project(self, request): # pragma: no cover cr.execute(sql_delete_cr_files, [project]) cr.execute(sql_delete_files, [project]) cr.execute(sql_delete_directories, [project]) + if cr.rowcount == 0: + _logger.info("No files or directories found for project %s" % project) + return Response(project, status=status.HTTP_404_NOT_FOUND) _logger.info("Permanently deleted all files and directories from project %s" % project) diff --git a/src/metax_api/api/rpc/base/views/statistic_rpc.py b/src/metax_api/api/rpc/base/views/statistic_rpc.py index 96c4713f..aa7479d1 100755 --- a/src/metax_api/api/rpc/base/views/statistic_rpc.py +++ b/src/metax_api/api/rpc/base/views/statistic_rpc.py @@ -131,6 +131,11 @@ def organization_datasets_cumulative(self, request): "to_date": request.query_params.get("to_date", None), "metadata_owner_org": request.query_params.get("metadata_owner_org", None), } + + for boolean_param in ["latest", "legacy", "removed"]: + if boolean_param in request.query_params: + params[boolean_param] = CS.get_boolean_query_param(request, boolean_param) + return Response(StatisticService.total_organization_datasets(**params)) @action(detail=False, methods=["get"], url_path="unused_files") diff --git a/src/metax_api/initialdata/datacatalogs.json b/src/metax_api/initialdata/datacatalogs.json index 539a26d1..4a604446 100755 --- a/src/metax_api/initialdata/datacatalogs.json +++ b/src/metax_api/initialdata/datacatalogs.json @@ -71,9 +71,11 @@ "research_dataset_schema": "ida", "logo": "fairdata_tree_logo.svg" }, - "catalog_record_services_edit": "ida,metax,qvain,qvain-light,tpas", - "catalog_record_services_create": "ida,metax,qvain,qvain-light,tpas", - "catalog_record_services_read": "ida,metax,qvain,qvain-light,etsin,tpas,download" + "catalog_record_services_edit": "ida,metax,qvain,qvain-light,tpas", + "catalog_record_services_create": "ida,metax,qvain,qvain-light,tpas", + "catalog_record_services_read": "ida,metax,qvain,qvain-light,etsin,tpas,download", + "publish_to_etsin": true, + "publish_to_ttv": true }, { "catalog_json": { @@ -148,9 +150,11 @@ "research_dataset_schema": "att", "logo": "fairdata_tree_logo.svg" }, - "catalog_record_services_edit": "ida,metax,qvain,qvain-light,tpas", - "catalog_record_services_create": "ida,metax,qvain,qvain-light,tpas", - "catalog_record_services_read": "ida,metax,qvain,qvain-light,etsin,tpas,download" + "catalog_record_services_edit": "ida,metax,qvain,qvain-light,tpas", + "catalog_record_services_create": "ida,metax,qvain,qvain-light,tpas", + "catalog_record_services_read": "ida,metax,qvain,qvain-light,etsin,tpas,download", + "publish_to_etsin": true, + "publish_to_ttv": true }, { "catalog_json": { @@ -226,11 +230,13 @@ "research_dataset_schema": "ida", "logo": "fairdata_tree_logo.svg" }, - "catalog_record_group_edit": "pas_edit", - "catalog_record_group_create": "pas_create", - "catalog_record_services_edit": "metax,tpas", - "catalog_record_services_create": "metax,tpas", - "catalog_record_services_read": "metax,tpas,download" + "catalog_record_group_edit": "pas_edit", + "catalog_record_group_create": "pas_create", + "catalog_record_services_edit": "metax,tpas", + "catalog_record_services_create": "metax,tpas", + "catalog_record_services_read": "metax,tpas,download", + "publish_to_etsin": true, + "publish_to_ttv": true }, { "catalog_json": { @@ -298,7 +304,9 @@ }, "catalog_record_services_edit": "metax,qvain,qvain-light,tpas", "catalog_record_services_create": "metax,qvain,qvain-light,tpas", - "catalog_record_services_read": "metax,qvain,qvain-light,tpas,download" + "catalog_record_services_read": "metax,qvain,qvain-light,tpas,download", + "publish_to_etsin": false, + "publish_to_ttv": false }, { "catalog_json": { @@ -354,9 +362,11 @@ "dataset_versioning": false, "research_dataset_schema": "dft" }, - "catalog_record_services_edit": "metax,qvain,qvain-light,tpas, ida, testuser, api_auth_user", - "catalog_record_services_create": "metax,qvain,qvain-light,tpas, ida, testuser, api_auth_user", - "catalog_record_services_read": "metax,qvain,qvain-light,etsin,tpas, ida, testuser, api_auth_user,download" + "catalog_record_services_edit": "metax,qvain,qvain-light,tpas, ida, testuser, api_auth_user", + "catalog_record_services_create": "metax,qvain,qvain-light,tpas, ida, testuser, api_auth_user", + "catalog_record_services_read": "metax,qvain,qvain-light,etsin,tpas, ida, testuser, api_auth_user,download", + "publish_to_etsin": true, + "publish_to_ttv": true }, { "catalog_json": { @@ -417,5 +427,251 @@ }, "catalog_record_services_edit": "metax,jyu", "catalog_record_services_create": "metax,jyu", - "catalog_record_services_read": "metax,jyu,download" + "catalog_record_services_read": "metax,jyu,download", + "publish_to_etsin": true, + "publish_to_ttv": true +}, +{ + "catalog_json": { + "title": { + "en": "Reportronic catalog", + "fi": "Reportronic katalogi" + }, + "language": [ + { + "identifier": "http://lexvo.org/id/iso639-3/fin" + } + ], + "harvested": false, + "publisher": { + "name": { + "en": "Reportronic", + "fi": "Reportronic" + } + }, + "identifier": "urn:nbn:fi:att:data-catalog-reportronic", + "access_rights": { + "license": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0" + } + ], + "access_type": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } + } + ], + "description": { + "en": "Contains datasets from Reportronic service", + "fi": "Sisältää aineistoja Reportronic-palvelusta" + } + }, + "dataset_versioning": false, + "research_dataset_schema": "att" + }, + "catalog_record_services_edit": "metax,reportronic", + "catalog_record_services_create": "metax,reportronic", + "catalog_record_services_read": "metax,reportronic", + "publish_to_etsin": false, + "publish_to_ttv": true +}, +{ + "catalog_json": { + "title": { + "en": "ACRIS catalog", + "fi": "ACRIS katalogi" + }, + "language": [ + { + "identifier": "http://lexvo.org/id/iso639-3/fin" + } + ], + "harvested": true, + "publisher": { + "name": { + "en": "Aalto ACRIS", + "fi": "Aalto ACRIS" + }, + "homepage": [{ + "title": { + "en": "https://research.aalto.fi", + "fi": "https://research.aalto.fi" + }, + "identifier": "https://research.aalto.fi" + }] + }, + "identifier": "urn:nbn:fi:att:data-catalog-acris", + "access_rights": { + "license": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0" + } + ], + "access_type": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } + } + ], + "description": { + "en": "Contains datasets from Aalto University's ACRIS system", + "fi": "Sisältää aineistoja Aalto yliopiston ACRIS-järjestelmästä" + } + }, + "dataset_versioning": false, + "research_dataset_schema": "att" + }, + "catalog_record_services_edit": "metax,aalto", + "catalog_record_services_create": "metax,aalto", + "catalog_record_services_read": "metax,aalto", + "publish_to_etsin": true, + "publish_to_ttv": true +}, +{ + "catalog_json": { + "title": { + "en": "Finnish Meteorological Institute EUDAT B2SHARE catalog", + "fi": "Ilmatieteen laitoksen EUDAT B2SHARE -katalogi", + "sv": "Meteorologiska Institutet EUDAT B2SHARE register" + }, + "language": [ + { + "title": { + "en": "Finnish language", + "fi": "Suomen kieli", + "sv": "finska", + "und": "Suomen kieli" + }, + "identifier": "http://lexvo.org/id/iso639-3/fin" + }, + { + "title": { + "en": "English language", + "fi": "Englannin kieli", + "sv": "engelska", + "und": "Englannin kieli" + }, + "identifier": "http://lexvo.org/id/iso639-3/eng" + } + ], + "harvested": true, + "publisher": { + "name": { + "en": "Finnish Meteorological Institute", + "fi": "Ilmatieteen laitos", + "sv": "Meteorologiska Institutet" + }, + "homepage": [ + { + "title": { + "en": "Home - Finnish Meteorological Institute", + "fi": "Etusivu - Ilmatieteen laitos", + "sv": "Startsida - Meteorologiska institutet" + }, + "identifier": "https://www.ilmatieteenlaitos.fi/" + } + ], + "identifier": "https://isni.org/isni/0000000122538678" + }, + "identifier": "urn:nbn:fi:att:data-catalog-fmi", + "access_rights": { + "license": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0" + } + ], + "access_type": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } + } + ], + "description": { + "en": "Contains datasets of Finnish Meteorological Institute harvested from FMI's EUDAT B2SHARE instance", + "fi": "Sisältää aineistoja, jotka on kerätty Ilmatieteen laitoksen EUDAT B2SHARE -instanssista" + } + }, + "dataset_versioning": false, + "research_dataset_schema": "att" + }, + "catalog_record_services_edit": "metax,eudat", + "catalog_record_services_create": "metax,eudat", + "catalog_record_services_read": "metax,eudat", + "publish_to_etsin": true, + "publish_to_ttv": true +}, +{ + "catalog_json": { + "title": { + "en": "CSC SD services datasets", + "fi": "CSC:n Arkaluonteisen datan palveluiden aineistokatalogi" + }, + "language": [ + { + "title": { + "en": "English language", + "fi": "Englannin kieli", + "sv": "engelska", + "und": "Englannin kieli" + }, + "identifier": "http://lexvo.org/id/iso639-3/eng" + } + ], + "harvested": true, + "publisher": { + "name": { + "en": "CSC Sensitive Data Services for Research" + }, + "homepage": [ + { + "title": { + "en": "Sensitive Data Services for Research - Services for Research - CSC Company Site" + }, + "identifier": "https://research.csc.fi/sensitive-data-services-for-research" + } + ] + }, + "identifier": "urn:nbn:fi:att:data-catalog-sd", + "access_rights": { + "license": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/license/code/CC-BY-4.0" + } + ], + "access_type": [ + { + "identifier": "http://uri.suomi.fi/codelist/fairdata/access_type/code/open", + "pref_label": { + "en": "Open", + "fi": "Avoin", + "und": "Avoin" + } + } + ], + "description": { + "en": "Datasets stored in CSC SD services", + "fi": "Sisältää aineistoja CSC:n SD-palvelusta" + } + }, + "dataset_versioning": false, + "research_dataset_schema": "att" + }, + "catalog_record_services_edit": "metax,sd", + "catalog_record_services_create": "metax,sd", + "catalog_record_services_read": "metax,sd", + "publish_to_etsin": true, + "publish_to_ttv": true }] \ No newline at end of file diff --git a/src/metax_api/management/commands/mark_files_removed.py b/src/metax_api/management/commands/mark_files_removed.py index 22c9ec86..b00bcfcc 100644 --- a/src/metax_api/management/commands/mark_files_removed.py +++ b/src/metax_api/management/commands/mark_files_removed.py @@ -32,6 +32,9 @@ def handle(self, *args, **options): removed_files_sum = 0 for prefix in path_prefixes: + if not prefix.strip(): + logger.info("Prefix is empty. Skipping.") + continue files = File.objects.filter(project_identifier = options["project_identifier"], file_path__startswith = prefix, removed = "f") logger.info(f"Found {len(files)} files to remove in project: {options['project_identifier']} with path prefix: {prefix}") for file in files: diff --git a/src/metax_api/middleware/identifyapicaller.py b/src/metax_api/middleware/identifyapicaller.py index 85b04e36..635de74a 100755 --- a/src/metax_api/middleware/identifyapicaller.py +++ b/src/metax_api/middleware/identifyapicaller.py @@ -108,7 +108,7 @@ def _identify_api_caller(self, request): "Unauthenticated access attempt from ip: %s. Authorization header missing" % request.META["REMOTE_ADDR"] ) - raise Http403 + raise Http403({"detail": ["Access denied."]}) try: auth_method, auth_b64 = http_auth_header.split(" ") @@ -156,10 +156,10 @@ def _auth_basic(self, request, auth_b64): if not user: _logger.warning("Failed authnz for user %s: user not found" % username) - raise Http403 + raise Http403({"detail": ["Access denied."]}) if apikey != user["password"]: _logger.warning("Failed authnz for user %s: password mismatch" % username) - raise Http403 + raise Http403({"detail": ["Access denied."]}) request.user.username = username request.user.is_service = True @@ -178,7 +178,7 @@ def _auth_bearer(self, request, auth_b64): if response.status_code != 200: _logger.warning("Bearer token validation failed") - raise Http403 + raise Http403({"detail": ["Access denied."]}) try: token = self._extract_id_token(auth_b64) @@ -186,13 +186,13 @@ def _auth_bearer(self, request, auth_b64): # the above method should never fail, as this code should not be # reachable if the token validation had already failed. _logger.exception("Failed to extract token from id_token string") - raise Http403 + raise Http403({"detail": ["Access denied."]}) if len(token.get("CSCUserName", "")) > 0: request.user.username = token["CSCUserName"] else: _logger.warning("id_token does not contain valid user id: fairdataid or cscusername") - raise Http403 + raise Http403({"detail": ["Access denied."]}) request.user.is_service = False request.user.token = token diff --git a/src/metax_api/middleware/stream_http_response.py b/src/metax_api/middleware/stream_http_response.py index 5ca379a9..5731743c 100755 --- a/src/metax_api/middleware/stream_http_response.py +++ b/src/metax_api/middleware/stream_http_response.py @@ -38,8 +38,9 @@ def __call__(self, request): and self._check_query_param(request, "stream") ): resp = StreamingHttpResponse(self._stream_response(response)) - resp._headers["content-type"] = ("Content-Type", "application/json") - resp._headers["x-count"] = ("X-Count", str(len(response.data))) + resp.headers["content-type"] = ("Content-Type", "application/json") + + resp.headers["x-count"] = ("X-Count", str(len(response.data))) return resp return response diff --git a/src/metax_api/migrations/0041_add_editorpermissions.py b/src/metax_api/migrations/0041_add_editorpermissions.py new file mode 100644 index 00000000..96a38c29 --- /dev/null +++ b/src/metax_api/migrations/0041_add_editorpermissions.py @@ -0,0 +1,195 @@ +# Generated by Django 3.1.13 on 2021-08-26 11:14 + +from django.db import migrations, models +import django.db.models.deletion +from django.utils import timezone +import logging +import uuid +from metax_api.models import catalog_record + +logger = logging.getLogger(__name__) + + +def add_permissions(apps, schema_editor): + """ + Add EditorPermissions for each version set and related next_draft CatalogRecords. + + Here CatalogRecords not belonging to a DatasetVersionSet are considered a version set with size 1. + """ + CatalogRecord = apps.get_model("metax_api", "CatalogRecordV2") + EditorUserPermission = apps.get_model("metax_api", "EditorUserPermission") + EditorPermissions = apps.get_model("metax_api", "EditorPermissions") + + num_perms = 0 + num_datasets = 0 + prev_version_set_id = None + version_set_users = [] + version_set_crs = [] + + def flush_version_set(): + """Create single EditorPermissions object for each version set, add creator user""" + nonlocal num_perms, num_datasets, version_set_crs, version_set_users + if len(version_set_crs) > 0: + permissions = EditorPermissions.objects.create() + permissions.catalog_records.add(*version_set_crs) + num_perms += 1 + num_datasets += len(version_set_crs) + + for user in version_set_users: + now = timezone.now().replace(microsecond=0) + EditorUserPermission.objects.create( + editor_permissions=permissions, + user_id=user, + verified=True, + role="creator", + date_created=now, + ) + version_set_users = [] + version_set_crs = [] + + # group datasets by version_sets and include their next_draft datasets + for cr in CatalogRecord.objects.filter(draft_of__isnull=True).order_by( + "dataset_version_set_id", "id" + ): + if cr.dataset_version_set_id is None or cr.dataset_version_set_id != prev_version_set_id: + flush_version_set() + + version_set_crs.append(cr) + if cr.next_draft: + version_set_crs.append(cr.next_draft) + + # DatasetVersionSet shouldn't have multiple metadata_provider_users but this supports them just in case + if cr.metadata_provider_user and cr.metadata_provider_user not in version_set_users: + version_set_users.append(cr.metadata_provider_user) + + prev_version_set_id = cr.dataset_version_set_id + flush_version_set() + + logger.info(f"Added {num_perms} EditorPermissions to {num_datasets} datasets") + + +def revert(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("metax_api", "0040_auto_20211006_1116"), + ] + + operations = [ + migrations.CreateModel( + name="EditorPermissions", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ], + ), + migrations.CreateModel( + name="EditorUserPermission", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("active", models.BooleanField(default=True)), + ("removed", models.BooleanField(default=False)), + ("date_modified", models.DateTimeField(null=True)), + ("user_modified", models.CharField(max_length=200, null=True)), + ("date_created", models.DateTimeField()), + ("user_created", models.CharField(max_length=200, null=True)), + ( + "service_modified", + models.CharField( + help_text="Name of the service who last modified the record", + max_length=200, + null=True, + ), + ), + ( + "service_created", + models.CharField( + help_text="Name of the service who created the record", + max_length=200, + null=True, + ), + ), + ("date_removed", models.DateTimeField(null=True)), + ("user_id", models.CharField(max_length=200)), + ( + "role", + models.CharField( + choices=[("creator", "Creator"), ("editor", "Editor")], + max_length=16, + ), + ), + ("verified", models.BooleanField(default=False)), + ("verification_token", models.CharField(max_length=32, null=True)), + ("verification_token_expires", models.DateTimeField(null=True)), + ( + "editor_permissions", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="users", + to="metax_api.editorpermissions", + ), + ), + ], + ), + migrations.AddField( + model_name="catalogrecord", + name="editor_permissions", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="catalog_records", + to="metax_api.editorpermissions", + ), + ), + migrations.AddIndex( + model_name="editoruserpermission", + index=models.Index(fields=["user_id"], name="metax_api_e_user_id_0b47cc_idx"), + ), + migrations.AddConstraint( + model_name="editoruserpermission", + constraint=models.UniqueConstraint( + fields=("editor_permissions", "user_id"), + name="unique_dataset_user_permission", + ), + ), + migrations.AddConstraint( + model_name="editoruserpermission", + constraint=models.CheckConstraint( + check=models.Q(_negated=True, user_id=""), name="require_user_id" + ), + ), + migrations.AddConstraint( + model_name="editoruserpermission", + constraint=models.CheckConstraint( + check=models.Q(role__in=["creator", "editor"]), name="require_role" + ), + ), + migrations.RunPython(add_permissions, revert), + migrations.AlterField( + model_name="catalogrecord", + name="editor_permissions", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="catalog_records", + to="metax_api.editorpermissions", + ), + ), + ] diff --git a/src/metax_api/migrations/0042_auto_20211108_1256.py b/src/metax_api/migrations/0042_auto_20211108_1256.py new file mode 100644 index 00000000..51e1afcb --- /dev/null +++ b/src/metax_api/migrations/0042_auto_20211108_1256.py @@ -0,0 +1,21 @@ +# Generated by Django 3.1.13 on 2021-11-08 10:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0041_add_editorpermissions'), + ] + + operations = [ + migrations.RemoveField( + model_name='editoruserpermission', + name='verification_token', + ), + migrations.RemoveField( + model_name='editoruserpermission', + name='verification_token_expires', + ), + ] diff --git a/src/metax_api/migrations/0043_remove_editoruserpermission_verified.py b/src/metax_api/migrations/0043_remove_editoruserpermission_verified.py new file mode 100644 index 00000000..220acfab --- /dev/null +++ b/src/metax_api/migrations/0043_remove_editoruserpermission_verified.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.13 on 2021-11-10 09:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0042_auto_20211108_1256'), + ] + + operations = [ + migrations.RemoveField( + model_name='editoruserpermission', + name='verified', + ), + ] diff --git a/src/metax_api/migrations/0044_change_aalto_catalog_to_harvested.py b/src/metax_api/migrations/0044_change_aalto_catalog_to_harvested.py new file mode 100755 index 00000000..54ca0604 --- /dev/null +++ b/src/metax_api/migrations/0044_change_aalto_catalog_to_harvested.py @@ -0,0 +1,34 @@ +# Generated by Django 3.1.13 on 2021-12-16 13:39 + +from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations + +import logging + +logger = logging.getLogger(__name__) + +def change_aalto_catalog_harvested_value(apps, schema_editor, harvested_value = True): + logger.info(f"Change Aalto catalog harvested value to: {harvested_value}") + try: + DataCatalog = apps.get_model('metax_api', 'DataCatalog') + aalto_catalog = DataCatalog.objects.get(catalog_json__identifier = "urn:nbn:fi:att:data-catalog-acris") + aalto_catalog.catalog_json["harvested"] = harvested_value + aalto_catalog.save() + except DataCatalog.DoesNotExist: + logger.info("Aalto catalog does not exist. Passing") + pass + +def revert(apps, schema_editor): + change_aalto_catalog_harvested_value(apps, schema_editor, harvested_value = False) + + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0043_remove_editoruserpermission_verified'), + ] + + operations = [ + migrations.RunPython(change_aalto_catalog_harvested_value, revert), + ] diff --git a/src/metax_api/migrations/0045_add_publish_fields_to_catalogs.py b/src/metax_api/migrations/0045_add_publish_fields_to_catalogs.py new file mode 100644 index 00000000..6c09ef02 --- /dev/null +++ b/src/metax_api/migrations/0045_add_publish_fields_to_catalogs.py @@ -0,0 +1,50 @@ +from django.db import migrations, models + +import logging + +logger = logging.getLogger(__name__) + + +def add_publish_fields_to_catalogs(apps, schema_editor): + logger.info("Add publish_to_etsin and publish_to_ttv fields to data catalogs") + + dont_publish_to_etsin = ["urn:nbn:fi:att:data-catalog-legacy", "urn:nbn:fi:att:data-catalog-reportronic", "urn:nbn:fi:att:data-catalog-repotronic"] + dont_publish_to_ttv = ["urn:nbn:fi:att:data-catalog-legacy"] + + DataCatalog = apps.get_model('metax_api', 'DataCatalog') + catalogs = DataCatalog.objects.all() + + for catalog in catalogs: + if catalog.catalog_json["identifier"] in dont_publish_to_etsin: + catalog.publish_to_etsin = False + + if catalog.catalog_json["identifier"] in dont_publish_to_ttv: + catalog.publish_to_ttv = False + + catalog.save() + + +def revert(apps, schema_editor): + pass + + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0044_change_aalto_catalog_to_harvested'), + ] + + operations = [ + migrations.AddField( + model_name='datacatalog', + name='publish_to_etsin', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='datacatalog', + name='publish_to_ttv', + field=models.BooleanField(default=True), + ), + migrations.RunPython(add_publish_fields_to_catalogs, revert), + ] diff --git a/src/metax_api/models/__init__.py b/src/metax_api/models/__init__.py index ec08fb44..643cac39 100755 --- a/src/metax_api/models/__init__.py +++ b/src/metax_api/models/__init__.py @@ -6,7 +6,7 @@ # :license: MIT from .api_error import ApiError -from .catalog_record import AlternateRecordSet, CatalogRecord +from .catalog_record import AlternateRecordSet, CatalogRecord, EditorPermissions, EditorUserPermission from .catalog_record_v2 import CatalogRecordV2 from .common import Common from .contract import Contract diff --git a/src/metax_api/models/catalog_record.py b/src/metax_api/models/catalog_record.py index 9a499bde..0c7dd9df 100755 --- a/src/metax_api/models/catalog_record.py +++ b/src/metax_api/models/catalog_record.py @@ -6,17 +6,21 @@ # :license: MIT import logging +import uuid from collections import defaultdict from copy import deepcopy +from datetime import datetime, timedelta from django.conf import settings from django.contrib.postgres.fields import ArrayField from django.db import connection, models, transaction from django.db.models import JSONField, Q, Sum from django.http import Http404 +from django.utils.crypto import get_random_string from rest_framework.serializers import ValidationError from metax_api.exceptions import Http400, Http403, Http503 +from metax_api.tasks.refdata.refdata_indexer import service from metax_api.utils import ( DelayedLog, IdentifierType, @@ -58,6 +62,61 @@ DFT_CATALOG = settings.DFT_DATA_CATALOG_IDENTIFIER +class EditorPermissions(models.Model): + """ + Shared permissions between linked copies of same dataset. + + Attaches a set of EditorUserPermission objects to a set of CatalogRecords. + """ + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + +class PermissionRole(models.TextChoices): + """Permission role for EditorPermission.""" + + CREATOR = "creator" + EDITOR = "editor" + + +class EditorUserPermission(Common): + """Table for attaching user roles to an EditorPermissions object.""" + + # Override inherited integer based id with uuid + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + # MODEL FIELD DEFINITIONS # + editor_permissions = models.ForeignKey( + EditorPermissions, related_name="users", on_delete=models.CASCADE + ) + user_id = models.CharField(max_length=200) + role = models.CharField(max_length=16, choices=PermissionRole.choices) + + class Meta: + indexes = [ + models.Index( + fields=[ + "user_id", + ] + ), + ] + constraints = [ + models.UniqueConstraint( + fields=["editor_permissions", "user_id"], name="unique_dataset_user_permission" + ), + models.CheckConstraint(check=~models.Q(user_id=""), name="require_user_id"), + models.CheckConstraint( + check=models.Q(role__in=PermissionRole.values), name="require_role" + ), + ] + + def __repr__(self): + return f"" + + def delete(self, *args, **kwargs): + super().remove(*args, **kwargs) + + class DiscardRecord(Exception): pass @@ -445,6 +504,10 @@ class CatalogRecord(Common): help_text="Saves api related info about the dataset. E.g. api version", ) + editor_permissions = models.ForeignKey( + EditorPermissions, related_name="catalog_records", null=False, on_delete=models.PROTECT + ) + # END OF MODEL FIELD DEFINITIONS # """ @@ -1252,6 +1315,12 @@ def preservation_dataset_origin_version_exists(self): """ return hasattr(self, "preservation_dataset_origin_version") + def catalog_publishes_to_etsin(self): + return self.data_catalog.publish_to_etsin + + def catalog_publishes_to_ttv(self): + return self.data_catalog.publish_to_ttv + def catalog_versions_datasets(self): return self.data_catalog.catalog_json.get("dataset_versioning", False) is True @@ -1392,16 +1461,24 @@ def _pre_create_operations(self, pid_type=None): elif self.catalog_is_legacy(): if "preferred_identifier" not in self.research_dataset: - raise ValidationError( - { - "detail": [ - "Selected catalog %s is a legacy catalog. Preferred identifiers are not " - "automatically generated for datasets stored in legacy catalogs, nor is " - "their uniqueness enforced. Please provide a value for dataset field " - "preferred_identifier." % self.data_catalog.catalog_json["identifier"] - ] - } - ) + + # Reportronic catalog does not need to validate unique identifiers + # Raise validation error when not reportronic catalog + if ( + self.data_catalog.catalog_json["identifier"] + != settings.REPORTRONIC_DATA_CATALOG_IDENTIFIER + ): + raise ValidationError( + { + "detail": [ + "Selected catalog %s is a legacy catalog. Preferred identifiers are not " + "automatically generated for datasets stored in legacy catalogs, nor is " + "their uniqueness enforced. Please provide a value for dataset field " + "preferred_identifier." + % self.data_catalog.catalog_json["identifier"] + ] + } + ) _logger.info( "Catalog %s is a legacy catalog - not generating pid" % self.data_catalog.catalog_json["identifier"] @@ -1442,7 +1519,10 @@ def _pre_create_operations(self, pid_type=None): if "remote_resources" in self.research_dataset: self._calculate_total_remote_resources_byte_size() - if not ("files" in self.research_dataset or "directories" in self.research_dataset) and "total_files_byte_size" in self.research_dataset: + if ( + not ("files" in self.research_dataset or "directories" in self.research_dataset) + and "total_files_byte_size" in self.research_dataset + ): self.research_dataset.pop("total_files_byte_size") if self.cumulative_state == self.CUMULATIVE_STATE_CLOSED: @@ -1458,6 +1538,12 @@ def _pre_create_operations(self, pid_type=None): self._set_api_version() + # only new datasets need new EditorPermissions, copies already have one + if not self.editor_permissions_id: + self._add_editor_permissions() + if self.metadata_provider_user: + self._add_creator_editor_user_permission() + def _post_create_operations(self): if "files" in self.research_dataset or "directories" in self.research_dataset: # files must be added after the record itself has been created, to be able @@ -1491,7 +1577,7 @@ def _post_create_operations(self): or self.use_doi_for_published is True ): self._validate_cr_against_datacite_schema() - if is_metax_generated_doi_identifier(self.research_dataset["preferred_identifier"]): + if is_metax_generated_doi_identifier(self.research_dataset.get("preferred_identifier")): self.add_post_request_callable( DataciteDOIUpdate(self, self.research_dataset["preferred_identifier"], "create") ) @@ -3033,6 +3119,27 @@ def _copy_undeleted_files_from_old_version(self): if DEBUG: _logger.debug("Added %d files to dataset %s" % (n_files_copied, self._new_version.id)) + def _add_editor_permissions(self): + permissions = EditorPermissions.objects.create() + self.editor_permissions = permissions + + def _add_creator_editor_user_permission(self): + """ + Add creator permission to a newly created CatalogRecord. + """ + perm = EditorUserPermission( + editor_permissions=self.editor_permissions, + user_id=self.metadata_provider_user, + role=PermissionRole.CREATOR, + date_created=self.date_created, + date_modified=self.date_modified, + user_created=self.user_created, + user_modified=self.user_modified, + service_created=self.service_created, + service_modified=self.service_modified, + ) + perm.save() + class RabbitMQPublishRecord: @@ -3057,11 +3164,6 @@ def __call__(self): """ from metax_api.services import RabbitMQService as rabbitmq - _logger.info( - "Publishing CatalogRecord %s to RabbitMQ... routing_key: %s" - % (self.cr.identifier, self.routing_key) - ) - if self.routing_key == "delete": cr_json = {"identifier": self.cr.identifier} else: @@ -3070,9 +3172,27 @@ def __call__(self): cr_json["data_catalog"] = {"catalog_json": self.cr.data_catalog.catalog_json} try: - for exchange in settings.RABBITMQ["EXCHANGES"]: - if exchange["EXC_TYPE"] == "dataset": - rabbitmq.publish(cr_json, routing_key=self.routing_key, exchange=exchange["NAME"]) + if self.cr.catalog_publishes_to_etsin(): + + _logger.info( + "Publishing CatalogRecord %s to RabbitMQ... exchange: datasets, routing_key: %s" + % (self.cr.identifier, self.routing_key) + ) + + rabbitmq.publish( + cr_json, routing_key=self.routing_key, exchange="datasets" + ) + if self.cr.catalog_publishes_to_ttv(): + + _logger.info( + "Publishing CatalogRecord %s to RabbitMQ... exchange: ttv-datasets, routing_key: %s" + % (self.cr.identifier, self.routing_key) + ) + + rabbitmq.publish( + cr_json, routing_key=self.routing_key, exchange="ttv-datasets" + ) + except: # note: if we'd like to let the request be a success even if this operation fails, # we could simply not raise an exception here. diff --git a/src/metax_api/models/catalog_record_v2.py b/src/metax_api/models/catalog_record_v2.py index b9ee5895..583a75c2 100755 --- a/src/metax_api/models/catalog_record_v2.py +++ b/src/metax_api/models/catalog_record_v2.py @@ -117,6 +117,12 @@ def _pre_create_operations(self): self._set_api_version() + # only new datasets need new EditorPermissions, copies already have one + if not self.editor_permissions_id: + self._add_editor_permissions() + if self.metadata_provider_user: + self._add_creator_editor_user_permission() + def _post_create_operations(self, pid_type=None): if "files" in self.research_dataset or "directories" in self.research_dataset: diff --git a/src/metax_api/models/data_catalog.py b/src/metax_api/models/data_catalog.py index 37e2c100..bd1b5a86 100755 --- a/src/metax_api/models/data_catalog.py +++ b/src/metax_api/models/data_catalog.py @@ -59,6 +59,13 @@ class DataCatalog(Common): help_text="Services which are allowed to read catalog records in the catalog.", ) + publish_to_etsin = models.BooleanField( + default=True + ) + publish_to_ttv = models.BooleanField( + default=True + ) + # END OF MODEL FIELD DEFINITIONS # READ_METHODS = ("GET", "HEAD", "OPTIONS") diff --git a/src/metax_api/models/directory.py b/src/metax_api/models/directory.py index 5b8c1714..bc2f68fd 100755 --- a/src/metax_api/models/directory.py +++ b/src/metax_api/models/directory.py @@ -8,7 +8,7 @@ import logging from django.db import connection, models -from django.db.models import Count, Prefetch, Sum +from django.db.models import Count, Prefetch, Sum, Q from .common import Common from .file import File @@ -71,23 +71,24 @@ def calculate_byte_size_and_file_count(self): ) update_statements = [] + annotated_root_directory = self._get_project_directory_tree(with_own_sizes=True) + annotated_root_directory._calculate_byte_size_and_file_count(update_statements) + + if len(update_statements) > 0: + sql_update_all_directories = """ + update metax_api_directory as d set + byte_size = results.byte_size, + file_count = results.file_count + from (values + %s + ) as results(byte_size, file_count, id) + where results.id = d.id; + """ % ",".join( + update_statements + ) - self._calculate_byte_size_and_file_count(update_statements) - - sql_update_all_directories = """ - update metax_api_directory as d set - byte_size = results.byte_size, - file_count = results.file_count - from (values - %s - ) as results(byte_size, file_count, id) - where results.id = d.id; - """ % ",".join( - update_statements - ) - - with connection.cursor() as cursor: - cursor.execute(sql_update_all_directories) + with connection.cursor() as cursor: + cursor.execute(sql_update_all_directories) _logger.info( "Project %s directory tree calculations complete. Total byte_size: " @@ -100,39 +101,59 @@ def calculate_byte_size_and_file_count(self): ) ) + def _get_project_directory_tree(self, with_own_sizes=False): + """ + Get all project directories from DB in single query. Returns current directory with + subdirectories in directory.sub_dirs. + + Optionally, annotate directories with total size and + count of files they contain as own_byte_size and own_file_count. + """ + project_directories = Directory.objects.filter( + project_identifier=self.project_identifier + ).only("file_count", "byte_size", "parent_directory_id") + + if with_own_sizes: + project_directories = project_directories.annotate( + own_byte_size=Sum("files__byte_size", filter=Q(files__removed=False)), + own_file_count=Count("files", filter=Q(files__removed=False)), + ) + + # build directory tree from annotated directories + directories_by_id = {d.id: d for d in project_directories} + for d in project_directories: + d.sub_dirs = [] + for d in project_directories: + if d.parent_directory_id is not None: + directories_by_id[d.parent_directory_id].sub_dirs.append(d) + annotated_root_directory = directories_by_id.get(self.id) + return annotated_root_directory + def _calculate_byte_size_and_file_count(self, update_statements): """ Recursively traverse the entire directory tree and update total byte size and file count for each directory. Accumulates a list of triplets for a big sql-update statement. """ + old_byte_size = self.byte_size + old_file_count = self.file_count self.byte_size = 0 self.file_count = 0 - # fields id, parent_directory_id must be specified for joining for Prefetch-object to work properly - sub_dirs = ( - self.child_directories.all() - .only("byte_size", "parent_directory_id") - .prefetch_related( - Prefetch( - "files", - queryset=File.objects.only("id", "byte_size", "parent_directory_id"), - ) - ) - ) - - if sub_dirs: - for sub_dir in sub_dirs: + if self.sub_dirs: + for sub_dir in self.sub_dirs: sub_dir._calculate_byte_size_and_file_count(update_statements) # sub dir numbers - self.byte_size = sum(d.byte_size for d in sub_dirs) - self.file_count = sum(d.file_count for d in sub_dirs) + self.byte_size = sum(d.byte_size for d in self.sub_dirs) + self.file_count = sum(d.file_count for d in self.sub_dirs) # note: never actually saved using .save() - self.byte_size += sum(f.byte_size for f in self.files.all()) or 0 - self.file_count += len(self.files.all()) or 0 + self.byte_size += self.own_byte_size or 0 + self.file_count += self.own_file_count or 0 - update_statements.append("(%d, %d, %d)" % (self.byte_size, self.file_count, self.id)) + # add updated values if changed + if self.byte_size != old_byte_size or self.file_count != old_file_count: + update_statements.append("(%d, %d, %d)" % (self.byte_size, self.file_count, self.id)) def calculate_byte_size_and_file_count_for_cr(self, cr_id, directory_data): """ @@ -156,7 +177,10 @@ def calculate_byte_size_and_file_count_for_cr(self, cr_id, directory_data): parent_id: (byte_size, file_count) for parent_id, byte_size, file_count in stats } - self._calculate_byte_size_and_file_count_for_cr(grouped_by_dir, directory_data) + directory_tree = self._get_project_directory_tree() + directory_tree._calculate_byte_size_and_file_count_for_cr( + grouped_by_dir, directory_data + ) def _calculate_byte_size_and_file_count_for_cr(self, grouped_by_dir, directory_data): """ @@ -173,15 +197,13 @@ def _calculate_byte_size_and_file_count_for_cr(self, grouped_by_dir, directory_d self.byte_size = 0 self.file_count = 0 - sub_dirs = self.child_directories.all().only("id") - - if sub_dirs: - for sub_dir in sub_dirs: + if self.sub_dirs: + for sub_dir in self.sub_dirs: sub_dir._calculate_byte_size_and_file_count_for_cr(grouped_by_dir, directory_data) # sub dir numbers - self.byte_size = sum(d.byte_size for d in sub_dirs) - self.file_count = sum(d.file_count for d in sub_dirs) + self.byte_size = sum(d.byte_size for d in self.sub_dirs) + self.file_count = sum(d.file_count for d in self.sub_dirs) current_dir = grouped_by_dir.get(self.id, [0, 0]) diff --git a/src/metax_api/services/catalog_record_service.py b/src/metax_api/services/catalog_record_service.py index 3f144272..73277998 100755 --- a/src/metax_api/services/catalog_record_service.py +++ b/src/metax_api/services/catalog_record_service.py @@ -130,8 +130,28 @@ def get_queryset_search_params(cls, request): queryset_search_params["api_meta__contains"] = {"version": value} + if request.query_params.get("editor_permissions_user"): + cls.filter_by_editor_permissions_user(request, queryset_search_params) + return queryset_search_params + @staticmethod + def filter_by_editor_permissions_user(request, queryset_search_params): + """ + Add filter for querying datasets where user has editor user permissions. + """ + user_id = request.query_params["editor_permissions_user"] + + # non-service users can only query their own datasets + if not request.user.is_service: + if request.user.username == '': + raise Http403({"detail": ["Query by editor_permissions_user is only supported for authenticated users"]}) + if request.user.username != user_id: + raise Http403({"detail": ["Provided editor_permissions_user does not match current user"]}) + + queryset_search_params["editor_permissions__users__user_id"] = user_id + queryset_search_params["editor_permissions__users__removed"] = False + @staticmethod def filter_by_state(request, queryset_search_params): """ diff --git a/src/metax_api/services/file_service.py b/src/metax_api/services/file_service.py index ad2d9617..ad0c983e 100755 --- a/src/metax_api/services/file_service.py +++ b/src/metax_api/services/file_service.py @@ -208,7 +208,8 @@ def restore_files(cls, request, file_identifier_list): removed = false, file_deleted = NULL, date_modified = CURRENT_TIMESTAMP, - user_modified = NULL + user_modified = NULL, + date_removed = NULL from (values %s ) as results(id, service_modified, parent_directory_id) @@ -440,8 +441,11 @@ def _mark_files_as_deleted(file_ids): _logger.info("Marking files as removed...") sql_delete_files = """ - update metax_api_file - set removed = true, file_deleted = CURRENT_TIMESTAMP, date_modified = CURRENT_TIMESTAMP + update metax_api_file set + removed = true, + file_deleted = CURRENT_TIMESTAMP, + date_modified = CURRENT_TIMESTAMP, + date_removed = CURRENT_TIMESTAMP where active = true and removed = false and id in %s""" diff --git a/src/metax_api/services/rabbitmq_service.py b/src/metax_api/services/rabbitmq_service.py index a4192717..bd7daba9 100755 --- a/src/metax_api/services/rabbitmq_service.py +++ b/src/metax_api/services/rabbitmq_service.py @@ -209,10 +209,11 @@ class _RabbitMQServiceDummy: """ def __init__(self, settings=settings): - pass + self.messages = [] def publish(self, body, routing_key="", exchange="datasets", persistent=True): - pass + msg = {"body":body, "routing_key":routing_key, "exchange":exchange, "persistent":persistent} + self.messages.append(msg) def init_exchanges(self, *args, **kwargs): pass diff --git a/src/metax_api/services/rems_service.py b/src/metax_api/services/rems_service.py index ffcbeb9f..766d6f8b 100755 --- a/src/metax_api/services/rems_service.py +++ b/src/metax_api/services/rems_service.py @@ -39,6 +39,7 @@ def __init__(self): self.reporter_user = settings["REPORTER_USER"] self.auto_approver = settings["AUTO_APPROVER"] self.form_id = settings["FORM_ID"] + self.organization = settings["ORGANIZATION"] self.headers = { "x-rems-api-key": self.api_key, @@ -60,6 +61,9 @@ def create_rems_entity(self, cr, user_info): """ self.cr = cr + # raise error if configured organization does not exist in REMS + self._get_rems_organization() + # create user. Successful even if userid is already taken self._post_rems("user", user_info) @@ -112,6 +116,12 @@ def _get_catalogue_item(self, rems_id): return rems_ci + def _get_rems_organization(self): + """ + Get configured organization object from REMS. + """ + return self._get_rems("organization", id=self.organization) + def _close_applications(self, rems_id, reason): """ Get all applications that are related to dataset and close them. @@ -153,7 +163,7 @@ def _close_entity(self, entity, id): def _create_workflow(self, user_id): body = { - "organization": self.cr.metadata_owner_org, + "organization": {"organization/id": self.organization}, "title": self.cr.research_dataset["preferred_identifier"], "type": "workflow/default", "handlers": [user_id], @@ -176,7 +186,11 @@ def _create_license(self): if any([v["textcontent"] == license_url for v in lic["localizations"].values()]): return lic["id"] - body = {"licensetype": "link", "localizations": {}} + body = { + "licensetype": "link", + "localizations": {}, + "organization": {"organization/id": self.organization}, + } for lang in list(license["title"].keys()): body["localizations"].update( @@ -190,7 +204,7 @@ def _create_license(self): def _create_resource(self, license_id): body = { "resid": self.cr.rems_identifier, - "organization": self.cr.metadata_owner_org, + "organization": {"organization/id": self.organization}, "licenses": [license_id], } @@ -207,6 +221,7 @@ def _create_catalogue_item(self, res_id, wf_id): "wfid": wf_id, "localizations": {}, "enabled": True, + "organization": {"organization/id": self.organization}, } for lang in list(rd_title.keys()): @@ -273,9 +288,17 @@ def _put_rems(self, entity, action, body): return resp - def _get_rems(self, entity, params=""): + def _get_rems(self, entity, params="", id=None): + """Get list of REMS entities or single entity by id. """ + if id: + id_path = f"/{id}" + else: + id_path = "" + try: - response = requests.get(f"{self.base_url}/{entity}s?{params}", headers=self.headers) + response = requests.get( + f"{self.base_url}/{entity}s{id_path}?{params}", headers=self.headers + ) except Exception as e: raise Exception(f"Connection to REMS failed while getting {entity}. Error: {e}") diff --git a/src/metax_api/services/statistic_service.py b/src/metax_api/services/statistic_service.py index 21d547e7..ac052619 100755 --- a/src/metax_api/services/statistic_service.py +++ b/src/metax_api/services/statistic_service.py @@ -131,13 +131,7 @@ def count_datasets( if legacy is not None: where_args.append( - "".join( - [ - "and dc.catalog_json->>'identifier'", - " = " if legacy else " != ", - "any(%s)", - ] - ) + ''.join(["and", " " if legacy else " NOT ", "dc.catalog_json->>'identifier'", " = ", "any(%s)"]) ) sql_args.append(settings.LEGACY_CATALOGS) @@ -210,13 +204,7 @@ def total_datasets(cls, from_date, to_date, latest=True, legacy=None, removed=No if legacy is not None: filter_sql.append( - "".join( - [ - "and dc.catalog_json->>'identifier'", - " = " if legacy else " != ", - "any(%s)", - ] - ) + ''.join(["and", " " if legacy else " NOT ", "dc.catalog_json->>'identifier'", " = ", "any(%s)"]) ) filter_args.append(settings.LEGACY_CATALOGS) @@ -345,7 +333,7 @@ def _total_data_catalog_datasets(cls, from_date, to_date, access_types, dc_id): return grouped @classmethod - def total_organization_datasets(cls, from_date, to_date, metadata_owner_org=None): + def total_organization_datasets(cls, from_date, to_date, metadata_owner_org=None, latest=True, legacy=None, removed=None): """ Retrieve dataset count and byte size per month and monthly cumulative for all organizations, or a given single organization, grouped by catalog. @@ -366,12 +354,12 @@ def total_organization_datasets(cls, from_date, to_date, metadata_owner_org=None results = {} for org in metadata_owner_orgs: - results[org] = cls._total_organization_datasets(from_date, to_date, org) + results[org] = cls._total_organization_datasets(from_date, to_date, org, latest, legacy, removed) return results @classmethod - def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): + def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org, latest, legacy, removed): sql = """ WITH cte AS ( SELECT @@ -382,6 +370,7 @@ def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): where dc.id = %s and state = 'published' and cr.metadata_owner_org = %s + OPTIONAL_WHERE_FILTERS GROUP BY mon ) SELECT @@ -403,12 +392,37 @@ def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): where dc.id = %s and state = 'published' and cr.metadata_owner_org = %s + OPTIONAL_WHERE_FILTERS GROUP BY mon ) cr USING (mon) GROUP BY mon, c.mon_ida_byte_size, count ORDER BY mon; """ + filter_sql = [] + filter_args = [] + + if latest: + filter_sql.append("and next_dataset_version_id is null") + + if removed is not None: + filter_sql.append("and cr.removed = %s") + filter_args.append(removed) + + if legacy is not None: + filter_sql.append( + "".join( + [ + "AND ", + "" if legacy else "NOT ", + "dc.catalog_json->>'identifier' = ANY(%s)", + ] + ) + ) + filter_args.append(settings.LEGACY_CATALOGS) + + sql = sql.replace("OPTIONAL_WHERE_FILTERS", "\n".join(filter_sql)) + catalogs = DataCatalog.objects.filter( catalog_json__research_dataset_schema__in=["ida", "att"] ).values() @@ -418,17 +432,8 @@ def _total_organization_datasets(cls, from_date, to_date, metadata_owner_org): with connection.cursor() as cr: for dc in catalogs: - cr.execute( - sql, - [ - dc["id"], - metadata_owner_org, - from_date, - to_date, - dc["id"], - metadata_owner_org, - ], - ) + sql_args = [dc["id"], metadata_owner_org] + filter_args + [from_date, to_date, dc["id"], metadata_owner_org] + filter_args + cr.execute(sql, sql_args) results = [ dict(zip([col[0] for col in cr.description], row)) for row in cr.fetchall() ] diff --git a/src/metax_api/settings/components/access_control.py b/src/metax_api/settings/components/access_control.py index 387c0c90..ebd99fd4 100755 --- a/src/metax_api/settings/components/access_control.py +++ b/src/metax_api/settings/components/access_control.py @@ -11,6 +11,7 @@ "datacatalogs": {}, "datasets": {}, "directories": {}, + "editorpermissions": {}, "files": {}, "filestorages": {}, "schemas": {}, @@ -36,6 +37,10 @@ class Role(Enum): API_AUTH_USER = "api_auth_user" EXTERNAL = "external" JYU = "jyu" + REPORTRONIC = "reportronic" + AALTO = "aalto" + EUDAT = "eudat" + SD = "sd" def __ge__(self, other): if self.__class__ is other.__class__: @@ -74,6 +79,8 @@ def __lt__(self, other): Role.TPAS, Role.QVAIN, Role.ETSIN, + Role.EUDAT, + Role.JYU, ] api_permissions.rest.datasets.read = [Role.ALL] api_permissions.rest.datasets["update"] = [ @@ -91,6 +98,32 @@ def __lt__(self, other): Role.ETSIN, ] +api_permissions.rest.editorpermissions.create = [ + Role.METAX, + Role.END_USERS, + Role.TPAS, + Role.QVAIN, + Role.QVAIN_LIGHT, + Role.ETSIN, +] +api_permissions.rest.editorpermissions.read = [Role.ALL] +api_permissions.rest.editorpermissions["update"] = [ + Role.METAX, + Role.END_USERS, + Role.TPAS, + Role.QVAIN, + Role.QVAIN_LIGHT, + Role.ETSIN, +] +api_permissions.rest.editorpermissions.delete = [ + Role.METAX, + Role.END_USERS, + Role.TPAS, + Role.QVAIN, + Role.QVAIN_LIGHT, + Role.ETSIN, +] + api_permissions.rest.directories.read = [ Role.METAX, Role.QVAIN, @@ -132,6 +165,7 @@ def __lt__(self, other): api_permissions.rpc.datasets.create_draft.use = [Role.ALL] api_permissions.rpc.datasets.create_new_version.use = [Role.ALL] api_permissions.rpc.datasets.fix_deprecated.use = [Role.ALL] +api_permissions.rpc.datasets.flush_user_data.use = [Role.METAX, Role.IDA, Role.TPAS] api_permissions.rpc.datasets.get_minimal_dataset_template.use = [Role.ALL] api_permissions.rpc.datasets.merge_draft.use = [Role.ALL] api_permissions.rpc.datasets.publish_dataset.use = [Role.ALL] diff --git a/src/metax_api/settings/components/common.py b/src/metax_api/settings/components/common.py index e422aeda..fcc0f34e 100755 --- a/src/metax_api/settings/components/common.py +++ b/src/metax_api/settings/components/common.py @@ -6,11 +6,16 @@ DEBUG = env("DEBUG") SECRET_KEY = env("DJANGO_SECRET_KEY") ADDITIONAL_USER_PROJECTS_PATH = env("ADDITIONAL_USER_PROJECTS_PATH") + IDA_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-ida" ATT_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-att" PAS_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-pas" LEGACY_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-legacy" DFT_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-dft" +REPORTRONIC_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-reportronic" +AALTO_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-acris" +FMI_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-fmi" +SD_DATA_CATALOG_IDENTIFIER = "urn:nbn:fi:att:data-catalog-sd" END_USER_ALLOWED_DATA_CATALOGS = [ IDA_DATA_CATALOG_IDENTIFIER, @@ -23,7 +28,9 @@ # catalogs where uniqueness of dataset pids is not enforced. LEGACY_CATALOGS = [ LEGACY_DATA_CATALOG_IDENTIFIER, + REPORTRONIC_DATA_CATALOG_IDENTIFIER, ] + VALIDATE_TOKEN_URL = env("VALIDATE_TOKEN_URL") CHECKSUM_ALGORITHMS = ["SHA-256", "MD5", "SHA-512"] ERROR_FILES_PATH = env("ERROR_FILES_PATH") @@ -139,10 +146,6 @@ DATABASES["default"]["ENGINE"] = "django.db.backends.postgresql" DATABASES["default"]["ATOMIC_REQUESTS"] = True -# Colorize automated test console output -RAINBOWTESTS_HIGHLIGHT_PATH = str(BASE_DIR) -TEST_RUNNER = "rainbowtests.test.runner.RainbowDiscoverRunner" - # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ @@ -183,7 +186,7 @@ if env("ENABLE_V2_ENDPOINTS"): API_VERSIONS_ENABLED.append("v2") -# Variables related to api credentials +# API credentials in development environment API_USERS = [ {"password": "test-metax", "username": "metax"}, {"password": "test-qvain", "username": "qvain"}, @@ -192,6 +195,9 @@ {"password": "test-etsin", "username": "etsin"}, {"password": "test-fds", "username": "fds"}, {"password": "test-download", "username": "download"}, + {"password": "test-eudat", "username": "eudat"}, + {"password": "test-jyu", "username": "jyu"}, + {"password": "test-sd", "username": "sd"}, ] SWAGGER_YAML_PATH = env('SWAGGER_YAML_PATH') diff --git a/src/metax_api/settings/components/logging.py b/src/metax_api/settings/components/logging.py index 93637a14..1d9e2e79 100755 --- a/src/metax_api/settings/components/logging.py +++ b/src/metax_api/settings/components/logging.py @@ -63,7 +63,7 @@ "metax_api": { "handlers": ["general", "console", "debug"], }, - "root": {"level": LOGGING_LEVEL, "handlers": ["console"]}, + "root": {"level": LOGGING_LEVEL, "handlers": ["console", "general"]}, }, } diff --git a/src/metax_api/settings/components/rems.py b/src/metax_api/settings/components/rems.py index 810c1ec7..abbcd718 100644 --- a/src/metax_api/settings/components/rems.py +++ b/src/metax_api/settings/components/rems.py @@ -19,5 +19,6 @@ REMS["REPORTER_USER"] = env("REMS_REPORTER_USER") REMS["AUTO_APPROVER"] = env("REMS_AUTO_APPROVER") REMS["FORM_ID"] = int(env("REMS_FORM_ID")) + REMS["ORGANIZATION"] = env("REMS_ORGANIZATION") except ImproperlyConfigured as e: logger.warning(e) diff --git a/src/metax_api/settings/environments/production.py b/src/metax_api/settings/environments/production.py index cf5223cb..30c9b856 100644 --- a/src/metax_api/settings/environments/production.py +++ b/src/metax_api/settings/environments/production.py @@ -1,6 +1,8 @@ from metax_api.settings.environments.stable import api_permissions, prepare_perm_values from metax_api.settings.environments.staging import API_USERS # noqa: F401 +api_permissions.rpc.datasets.flush_user_data.use.clear() + api_permissions.rpc.files.flush_project.use.clear() API_ACCESS = prepare_perm_values(api_permissions.to_dict()) diff --git a/src/metax_api/settings/environments/stable.py b/src/metax_api/settings/environments/stable.py index 2f59d398..b5c57b3a 100644 --- a/src/metax_api/settings/environments/stable.py +++ b/src/metax_api/settings/environments/stable.py @@ -1,9 +1,9 @@ from metax_api.settings.components.access_control import Role, api_permissions, prepare_perm_values from metax_api.settings.environments.staging import API_USERS # noqa: F401 -api_permissions.rest.datasets.create += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU] -api_permissions.rest.datasets["update"] += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU] -api_permissions.rest.datasets.delete += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU] +api_permissions.rest.datasets.create += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU, Role.REPORTRONIC, Role.AALTO, Role.EUDAT, Role.SD] +api_permissions.rest.datasets["update"] += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU, Role.REPORTRONIC, Role.AALTO, Role.EUDAT, Role.SD] +api_permissions.rest.datasets.delete += [Role.IDA, Role.QVAIN_LIGHT, Role.JYU, Role.REPORTRONIC, Role.AALTO, Role.EUDAT, Role.SD] api_permissions.rest.directories.read += [Role.IDA, Role.QVAIN_LIGHT] diff --git a/src/metax_api/settings/environments/staging.py b/src/metax_api/settings/environments/staging.py index e9c1675a..88ca063e 100644 --- a/src/metax_api/settings/environments/staging.py +++ b/src/metax_api/settings/environments/staging.py @@ -31,6 +31,7 @@ api_permissions.rest.filestorages.delete = [Role.ALL] api_permissions.rpc.datasets.set_preservation_identifier.use = [Role.ALL] +api_permissions.rpc.datasets.flush_user_data.use = [Role.ALL] api_permissions.rpc.files.delete_project.use = [Role.ALL] api_permissions.rpc.files.flush_project.use = [Role.ALL] diff --git a/src/metax_api/settings/environments/unittests.py b/src/metax_api/settings/environments/unittests.py index 631d9182..e6950902 100755 --- a/src/metax_api/settings/environments/unittests.py +++ b/src/metax_api/settings/environments/unittests.py @@ -58,3 +58,19 @@ api_permissions.rpc.files.delete_project.use += [Role.TEST_USER] API_ACCESS = prepare_perm_values(api_permissions.to_dict()) + +from metax_api.settings.components.rems import REMS + +REMS.update( + { + "ENABLED": True, + "API_KEY": "key", + "BASE_URL": "https://mock-rems/api", + "ETSIN_URL_TEMPLATE": "https://etsin.fd-dev.csc.fi/dataset/%s", + "METAX_USER": "rems-metax@example.com", + "REPORTER_USER": "rems-reporter@example.com", + "AUTO_APPROVER": "not-used", + "FORM_ID": 1, + "ORGANIZATION": "rems-test-org", + } +) diff --git a/src/metax_api/swagger/v1/swagger.yaml b/src/metax_api/swagger/v1/swagger.yaml index 9c76ecc8..cb605be1 100755 --- a/src/metax_api/swagger/v1/swagger.yaml +++ b/src/metax_api/swagger/v1/swagger.yaml @@ -640,7 +640,7 @@ paths: in: query description: Sets paging on with default limit of 10 required: false - type: bolean + type: boolean - name: offset in: query description: Offset for paging @@ -1414,8 +1414,133 @@ paths: description: Resource not found. tags: - Dataset API - - + /rest/datasets/{CRID}/editor_permissions/users: + get: + summary: List all editor permissions of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + responses: + "200": + description: Successful operation, return a list of editor rights. May return an empty list. + schema: + type: array + items: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + post: + summary: Create a new editor permission of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: body + in: body + schema: + type: object + properties: + user_id: + type: string + role: + type: string + responses: + "201": + description: Successful operation, return created editor rights. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + /rest/datasets/{CRID}/editor_permissions/users/{USER_ID}: + get: + summary: List all editor permissions of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + responses: + "200": + description: Successful operation, return a list of editor rights. May return an empty list. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + patch: + summary: Update role or enable verified + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + - name: body + in: body + schema: + $ref: '#/definitions/EditorUserPermission' + responses: + "200": + description: Successful operation, return changed editor rights. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + delete: + summary: Editorpermission marked as removed + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + responses: + '200': + description: Successful operation + '400': + description: Bad request + '403': + description: Forbidden + '404': + description: Not found + tags: + - Dataset API # Contract API /rest/contracts: get: @@ -1695,6 +1820,25 @@ paths: description: Successful operation, returns information about the new dataset version. tags: - Dataset RPC + /rpc/datasets/flush_user_data: + post: + summary: Flushes user's datasets + description: Usable by ida, tpas and metax users. Permanently deletes datasets created by a certain user. Only for non-production use. + parameters: + - name: metadata_provider_user + in: query + description: Identifies the user whose datasets will be flushed. + required: true + type: string + responses: + '204': + description: Successful operation, returns no content. + '400': + description: Required parameter was missing or API was called in production environment. Response includes details. + '404': + description: No datasets were found for requested user. + tags: + - Dataset RPC /rpc/statistics/count_datasets: get: summary: Get the total number of datasets and file sizes. @@ -1830,6 +1974,9 @@ paths: - $ref: "#/parameters/from_date" - $ref: "#/parameters/to_date" - $ref: "#/parameters/metadata_owner_org_filter" + - $ref: "#/parameters/latest_filter" + - $ref: "#/parameters/removed_filter" + - $ref: "#/parameters/legacy_filter" responses: '200': description: Successful operation. @@ -1867,6 +2014,25 @@ paths: description: Unsuccesful operation when project identifier is missing. tags: - File RPC + /rpc/files/flush_project: + post: + summary: Flushes given project from the database. + description: Usable by ida, tpas and metax users. Permanently deletes given project's files and directories. Only for non-production use. + parameters: + - name: project_identifier + in: query + description: Identifies the project whose files and directories will be flushed. + required: true + type: string + responses: + '204': + description: Successful operation, returns no content. + '400': + description: Required parameter was missing or API was called in production environment. Response includes details. + '404': + description: No files or directories were found for requested project. + tags: + - File RPC parameters: @@ -2043,8 +2209,43 @@ definitions: count_cumulative: type: number format: float - - + EditorUserPermission: + type: object + properties: + id: + type: integer + readOnly: true + active: + type: boolean + removed: + type: boolean + date_modified: + type: string + format: date-time + readOnly: true + user_modified: + type: string + date_created: + type: string + format: date-time + readOnly: true + user_created: + type: string + service_modified: + type: string + service_created: + type: string + date_removed: + type: string + format: date-time + user_id: + type: string + role: + type: string + verified: + type: boolean + editor_permission_id: + type: integer examples: count_datasets: type: object diff --git a/src/metax_api/swagger/v2/swagger.yaml b/src/metax_api/swagger/v2/swagger.yaml index 682ffdf8..51a88991 100755 --- a/src/metax_api/swagger/v2/swagger.yaml +++ b/src/metax_api/swagger/v2/swagger.yaml @@ -1598,7 +1598,133 @@ paths: description: Resource not found. tags: - Dataset API - + /rest/datasets/{CRID}/editor_permissions/users: + get: + summary: List all editor permissions of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + responses: + "200": + description: Successful operation, return a list of editor rights. May return an empty list. + schema: + type: array + items: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + post: + summary: Create a new editor permission of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: body + in: body + schema: + type: object + properties: + user_id: + type: string + role: + type: string + responses: + "201": + description: Successful operation, return created editor rights. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + /rest/datasets/{CRID}/editor_permissions/users/{USER_ID}: + get: + summary: List all editor permissions of a record + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + responses: + "200": + description: Successful operation, return a list of editor rights. May return an empty list. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + patch: + summary: Update role + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + - name: body + in: body + schema: + $ref: '#/definitions/EditorUserPermission' + responses: + "200": + description: Successful operation, return changed editor rights. + schema: + $ref: '#/definitions/EditorUserPermission' + "400": + description: Bad request. + "404": + description: Resource not found. + tags: + - Dataset API + delete: + summary: Editorpermission marked as removed + parameters: + - name: CRID + in: path + description: Catalog record ID + required: true + type: string + - name: USER_ID + in: path + description: User ID + required: true + type: string + responses: + '200': + description: Successful operation + '400': + description: Bad request + '403': + description: Forbidden + '404': + description: Not found + tags: + - Dataset API # Contract API /rest/v2/contracts: @@ -1974,6 +2100,25 @@ paths: description: Not found tags: - Dataset RPC + /rpc/v2/datasets/flush_user_data: + post: + summary: Flushes user's datasets + description: Usable by ida, tpas and metax users. Permanently deletes datasets created by a certain user. Only for non-production use. + parameters: + - name: metadata_provider_user + in: query + description: Identifies the user whose datasets will be flushed. + required: true + type: string + responses: + '204': + description: Successful operation, returns no content. + '400': + description: Required parameter was missing or API was called in production environment. Response includes details. + '404': + description: No datasets were found for requested user. + tags: + - Dataset RPC /rpc/v2/statistics/count_datasets: get: summary: Get the total number of datasets and file sizes. @@ -2109,6 +2254,9 @@ paths: - $ref: "#/parameters/from_date" - $ref: "#/parameters/to_date" - $ref: "#/parameters/metadata_owner_org_filter" + - $ref: "#/parameters/latest_filter" + - $ref: "#/parameters/removed_filter" + - $ref: "#/parameters/legacy_filter" responses: '200': description: Successful operation. @@ -2146,6 +2294,25 @@ paths: description: Unsuccesful operation when project identifier is missing. tags: - File RPC + /rpc/v2/files/flush_project: + post: + summary: Flushes given project from the database. + description: Usable by ida, tpas and metax users. Permanently deletes given project's files and directories. Only for non-production use. + parameters: + - name: project_identifier + in: query + description: Identifies the project whose files and directories will be flushed. + required: true + type: string + responses: + '204': + description: Successful operation, returns no content. + '400': + description: Required parameter was missing or API was called in production environment. Response includes details. + '404': + description: No files or directories were found for requested project. + tags: + - File RPC parameters: @@ -2389,7 +2556,41 @@ definitions: count_cumulative: type: number format: float - + EditorUserPermission: + type: object + properties: + id: + type: integer + readOnly: true + active: + type: boolean + removed: + type: boolean + date_modified: + type: string + format: date-time + readOnly: true + user_modified: + type: string + date_created: + type: string + format: date-time + readOnly: true + user_created: + type: string + service_modified: + type: string + service_created: + type: string + date_removed: + type: string + format: date-time + user_id: + type: string + role: + type: string + editor_permission_id: + type: integer examples: count_datasets: diff --git a/src/metax_api/tests/api/rest/base/views/common/read.py b/src/metax_api/tests/api/rest/base/views/common/read.py index 6d4887d7..779508ca 100755 --- a/src/metax_api/tests/api/rest/base/views/common/read.py +++ b/src/metax_api/tests/api/rest/base/views/common/read.py @@ -142,7 +142,6 @@ def test_pagination_ordering(self): self.assertEqual(from_api, from_db) - class ApiReadHTTPHeaderTests(CatalogRecordApiReadCommon): # # header if-modified-since tests, single @@ -193,7 +192,7 @@ def test_get_with_if_modified_since_header_syntax_error(self): # List operation returns always 200 even if no datasets match the if-modified-since criterium - def test_list_get_with_if_modified_since_header_ok(self): + def _test_list_get_with_if_modified_since_header_ok(self): cr = CatalogRecord.objects.get(pk=self.pk) date_modified = cr.date_modified date_modified_in_gmt = timezone.localtime(date_modified, timezone=tz("GMT")) @@ -234,6 +233,16 @@ def test_list_get_with_if_modified_since_header_ok(self): self.assertTrue(len(response.data.get("results")) > 6) self.assertTrue(len(response.data.get("results")) == 28) + def test_list_get_with_if_modified_since_header_ok_as_unauthenticated_user(self): + + self._set_http_authorization("no") + self._test_list_get_with_if_modified_since_header_ok() + + def test_list_get_with_if_modified_since_header_ok_as_authenticated_user(self): + + self._use_http_authorization() + self._test_list_get_with_if_modified_since_header_ok() + class ApiReadQueryParamTests(CatalogRecordApiReadCommon): diff --git a/src/metax_api/tests/api/rest/base/views/datasets/read.py b/src/metax_api/tests/api/rest/base/views/datasets/read.py index 98b255bc..688e99f2 100755 --- a/src/metax_api/tests/api/rest/base/views/datasets/read.py +++ b/src/metax_api/tests/api/rest/base/views/datasets/read.py @@ -965,6 +965,19 @@ def test_filter_by_legacy(self): self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) self.assertEqual(count_all, response.data["count"], response.data) + def test_filter_by_editor_permissions_user_ok(self): + cr = CatalogRecord.objects.get(pk=1) + cr.editor_permissions.users.update(user_id='test_user_x') + response = self.client.get(f"/rest/datasets?editor_permissions_user=test_user_x") + self.assertEqual(response.data["count"], 1) + + def test_filter_by_editor_permissions_user_removed(self): + cr = CatalogRecord.objects.get(pk=1) + cr.editor_permissions.users.update(user_id='test_user_x') + cr.editor_permissions.users.first().delete() + response = self.client.get(f"/rest/datasets?editor_permissions_user=test_user_x") + self.assertEqual(response.data["count"], 0) + class CatalogRecordApiReadXMLTransformationTests(CatalogRecordApiReadCommon): @@ -1068,12 +1081,8 @@ def test_read_dataset_format_dummy_datacite_doi(self): def _check_dataset_xml_format_response(self, response, element_name): self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual("content-type" in response._headers, True, response._headers) - self.assertEqual( - "application/xml" in response._headers["content-type"][1], - True, - response._headers, - ) + + self.assertEqual(" +# :license: MIT + +from json import load as json_load +import uuid +import responses + +from django.core.management import call_command + +from rest_framework import status +from rest_framework.test import APITestCase + +from metax_api.tests.utils import TestClassUtils, test_data_file_path + + +class EditorUserPermissionApiReadCommon(APITestCase, TestClassUtils): + @classmethod + def setUpClass(cls): + """Loaded only once for test cases inside this class.""" + call_command("loaddata", test_data_file_path, verbosity=0) + super(EditorUserPermissionApiReadCommon, cls).setUpClass() + + def setUp(self): + self.cr_from_test_data = self._get_whole_object_from_test_data( + "catalogrecord", requested_pk=1 + ) + self.crid = self.cr_from_test_data["pk"] + self.identifier = "cr955e904-e3dd-4d7e-99f1-3fed446f96d1" + self.permissionid = self.cr_from_test_data["fields"]["editor_permissions_id"] + self.metadata_provider_user = self.cr_from_test_data["fields"]["metadata_provider_user"] + self.editor_user_permission = self._get_whole_object_from_test_data( + "editoruserpermission", requested_pk=str(uuid.UUID(int=1)) + ) + self.userid = self.editor_user_permission["fields"]["user_id"] + self._use_http_authorization() + + def _get_whole_object_from_test_data(self, model_name, requested_pk=0): + + with open(test_data_file_path) as test_data_file: + test_data = json_load(test_data_file) + + model = "metax_api.%s" % model_name + + for row in test_data: + if row["model"] == model: + if row["pk"] == requested_pk: + obj = { + "id": row["pk"], + } + obj.update(row) + return obj + + raise Exception( + "Could not find model %s from test data with pk == %d. " + "Are you certain you generated rows for model %s in generate_test_data.py?" + % (model_name, requested_pk, model_name) + ) + + +class EditorUserPermissionApiReadBasicTests(EditorUserPermissionApiReadCommon): + + """Basic read operations.""" + + def test_read_editor_permission_list_with_pk(self): + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_read_editor_permission_list_with_uuid(self): + response = self.client.get("/rest/datasets/%s/editor_permissions/users" % self.identifier) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_read_editor_permission_list_invalid(self): + response = self.client.get("/rest/datasets/99999/editor_permissions/users") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_read_editor_permission_details_by_pk(self): + response = self.client.get( + "/rest/datasets/%d/editor_permissions/users/%s" % (self.crid, self.userid) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(str(response.data["editor_permissions"]), self.permissionid) + self.assertEqual(response.data["user_id"], self.userid) + + def test_read_editor_permission_details_by_pk_invalid(self): + response = self.client.get( + "/rest/datasets/%d/editor_permissions/users/%s" % (self.crid, "invalid") + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + @responses.activate + def test_read_editor_permission_list_by_wrong_user(self): + self._mock_token_validation_succeeds() + self._use_http_authorization( + method="bearer", token={"group_names": [], "CSCUserName": "not_dataset_creator"} + ) + response = self.client.get(f"/rest/datasets/{self.crid}/editor_permissions/users") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + @responses.activate + def test_read_editor_permission_list_by_provider_user(self): + self._mock_token_validation_succeeds() + self._use_http_authorization( + method="bearer", token={"group_names": [], "CSCUserName": self.metadata_provider_user} + ) + response = self.client.get(f"/rest/datasets/{self.crid}/editor_permissions/users") + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/src/metax_api/tests/api/rest/base/views/editorpermissions/write.py b/src/metax_api/tests/api/rest/base/views/editorpermissions/write.py new file mode 100644 index 00000000..99ccc858 --- /dev/null +++ b/src/metax_api/tests/api/rest/base/views/editorpermissions/write.py @@ -0,0 +1,219 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT + + +from json import load as json_load +import uuid + +import responses +from django.core.management import call_command + +from rest_framework import status +from rest_framework.test import APITestCase + +from metax_api.models import CatalogRecord, EditorUserPermission +from metax_api.tests.utils import TestClassUtils, test_data_file_path + + +class EditorUserPermissionApiWriteCommon(APITestCase, TestClassUtils): + @classmethod + def setUpClass(cls): + """ + Loaded only once for test cases inside this class. + """ + call_command("loaddata", test_data_file_path, verbosity=0) + super(EditorUserPermissionApiWriteCommon, cls).setUpClass() + + def setUp(self): + self.cr_from_test_data = self._get_whole_object_from_test_data( + "catalogrecord", requested_pk=1 + ) + self.crid = self.cr_from_test_data["pk"] + self.permissionid = self.cr_from_test_data["fields"]["editor_permissions_id"] + self.editor_user_permission = self._get_whole_object_from_test_data( + "editoruserpermission", requested_pk=str(uuid.UUID(int=1)) + ) + self.metadata_provider_user = self.cr_from_test_data["fields"]["metadata_provider_user"] + self.userid = self.editor_user_permission["fields"]["user_id"] + self._use_http_authorization() + + def _get_whole_object_from_test_data(self, model_name, requested_pk=0): + + with open(test_data_file_path) as test_data_file: + test_data = json_load(test_data_file) + + model = "metax_api.%s" % model_name + + for row in test_data: + if row["model"] == model: + if row["pk"] == requested_pk: + obj = { + "id": row["pk"], + } + obj.update(row) + return obj + + raise Exception( + "Could not find model %s from test data with pk == %d. " + "Are you certain you generated rows for model %s in generate_test_data.py?" + % (model_name, requested_pk, model_name) + ) + + +class EditorUserPermissionApiWriteBasicTests(EditorUserPermissionApiWriteCommon): + + """ + Basic read operations + """ + + def test_write_editor_permission(self): + self._set_http_authorization("service") + data = {"role": "editor", "user_id": "test_editor"} + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + user_count = len(response.data) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + self.assertEqual(str(response.data["editor_permissions"]), self.permissionid) + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), user_count + 1) + + def test_write_editor_permission_invalid_data(self): + self._set_http_authorization("service") + data = {"role": "editor"} + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + user_count = len(response.data) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), user_count) + + def test_write_editor_permission_existing_userid(self): + self._set_http_authorization("service") + data = {"role": "editor", "user_id": "double_editor"} + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + user_count = len(response.data) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), user_count + 1) + + def test_write_editor_permission_change_values(self): + self._set_http_authorization("service") + data = {"role": "creator", "user_id": "change_editor"} + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + new_data = {"role": "editor"} + response = self.client.patch( + "/rest/datasets/%d/editor_permissions/users/%s" + % (self.crid, response.data.get("user_id")), + new_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.assertEqual(response.data.get("role"), "editor") + + def test_write_editor_permission_remove_users(self): + self._set_http_authorization("service") + data = {"role": "creator", "user_id": "new_creator"} + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + response = self.client.delete( + "/rest/datasets/%d/editor_permissions/users/%s" % (self.crid, data.get("user_id")) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + + response = self.client.get("/rest/datasets/%d/editor_permissions/users" % self.crid) + for user in response.data: + if user.get("role") == "creator": + response = self.client.delete( + "/rest/datasets/%d/editor_permissions/users/%s" + % (self.crid, user.get("user_id")) + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.data) + else: + response = self.client.delete( + "/rest/datasets/%d/editor_permissions/users/%s" + % (self.crid, user.get("user_id")) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + + def test_write_editor_permission_add_removed_user(self): + self._set_http_authorization("service") + data = {"role": "editor", "user_id": "new_editor"} + # add + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + # remove + response = self.client.delete( + "/rest/datasets/%d/editor_permissions/users/%s" % (self.crid, data.get("user_id")) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + removed_user = EditorUserPermission.objects_unfiltered.get( + user_id="new_editor", editor_permissions_id=self.permissionid + ) + self.assertEqual(removed_user.removed, True) + response = self.client.post( + "/rest/datasets/%d/editor_permissions/users" % self.crid, data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + self.assertEqual(response.data.get("removed"), False) + + @responses.activate + def test_write_editor_permission_not_dataset_creator(self): + self._mock_token_validation_succeeds() + self._use_http_authorization( + method="bearer", token={"group_names": [], "CSCUserName": "not_dataset_creator"} + ) + data = {"role": "editor", "user_id": "test_editor"} + response = self.client.post( + f"/rest/datasets/{self.crid}/editor_permissions/users", data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + + @responses.activate + def test_write_editor_permission_provider_user(self): + self._mock_token_validation_succeeds() + self._use_http_authorization( + method="bearer", token={"group_names": [], "CSCUserName": self.metadata_provider_user} + ) + data = {"role": "editor", "user_id": "test_editor"} + response = self.client.post( + f"/rest/datasets/{self.crid}/editor_permissions/users", data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + def test_write_editor_permission_list_service_with_no_write_access(self): + cr = CatalogRecord.objects.get(pk=self.crid) + cr.data_catalog.catalog_record_services_edit = "" + cr.data_catalog.save() + self._set_http_authorization("service") + data = {"role": "editor", "user_id": "test_editor"} + response = self.client.post( + f"/rest/datasets/{self.crid}/editor_permissions/users", data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/src/metax_api/tests/api/rest/base/views/schemas/read.py b/src/metax_api/tests/api/rest/base/views/schemas/read.py index 88122020..10c11583 100755 --- a/src/metax_api/tests/api/rest/base/views/schemas/read.py +++ b/src/metax_api/tests/api/rest/base/views/schemas/read.py @@ -21,7 +21,7 @@ def test_read_schemas_list_html(self): headers = {"HTTP_ACCEPT": "text/html"} response = self.client.get("/rest/schemas", **headers) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response._headers["content-type"][1].find("json") >= 0) + # self.assertTrue(response.headers["content-type"][1].find("json") >= 0) def test_read_schema_retrieve_existing(self): list_response = self.client.get("/rest/schemas") diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py b/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py index 710ecfef..398e661c 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/drafts.py @@ -612,6 +612,22 @@ def test_create_and_merge_draft(self): ) self.assertEqual("next_draft" in response.data, False, "next_draft link should be gone") + def test_create_and_merge_draft_keeps_permissions(self): + """ + Ensure creating and merging drafts keeps the same EditorPermission object. + """ + cr = self._create_dataset() + original_editor_permissions_id = CatalogRecordV2.objects.get(id=cr['id']).editor_permissions_id + + draft_cr = self._create_draft(cr["id"]) + draft_editor_permissions_id = CatalogRecordV2.objects.get(id=draft_cr['id']).editor_permissions_id + self.assertEqual(draft_editor_permissions_id, original_editor_permissions_id) + + self._merge_draft_changes(draft_cr["id"]) + merged_editor_permissions_id = CatalogRecordV2.objects.get(id=cr['id']).editor_permissions_id + self.assertEqual(merged_editor_permissions_id, original_editor_permissions_id) + + def test_missing_issued_date_is_generated_when_draft_is_merged(self): """ Testing a case where user removes 'issued_date' from draft before merging diff --git a/src/metax_api/tests/api/rest/v2/views/datasets/write.py b/src/metax_api/tests/api/rest/v2/views/datasets/write.py index 43f3d012..7f30506f 100755 --- a/src/metax_api/tests/api/rest/v2/views/datasets/write.py +++ b/src/metax_api/tests/api/rest/v2/views/datasets/write.py @@ -13,6 +13,7 @@ from rest_framework.test import APITestCase from metax_api.models import CatalogRecordV2, DataCatalog +from metax_api.services import RabbitMQService from metax_api.tests.utils import TestClassUtils, test_data_file_path CR = CatalogRecordV2 @@ -654,4 +655,73 @@ def test_external_service_can_not_add_catalog_record_to_other_catalog(self): self.cr_test_data["research_dataset"]["preferred_identifier"] = "temp-pid" response = self.client.post("/rest/v2/datasets", self.cr_test_data, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) \ No newline at end of file + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, response.data) + +class CatalogRecordRabbitMQPublish(CatalogRecordApiWriteCommon): + """ + Testing if RabbitMQ messages are published to correct exchanges with correct routing keys + when creating, updating, or deleting a catalog record. + Uses a dummy RabbitMQService. + """ + + def setUp(self): + super().setUp() + + def _check_rabbitmq_queue(self, routing_key, publish_to_etsin, publish_to_ttv): + """ + Checks if a message with a given routing key exists in the correct exchange + in the dummy RabbitMQService queue + """ + messages_str = ''.join(str(message) for message in RabbitMQService.messages) + assert_str_etsin = f"'routing_key': '{routing_key}', 'exchange': 'datasets'" + assert_str_ttv = f"'routing_key': '{routing_key}', 'exchange': 'ttv-datasets'" + + self.assertEqual(assert_str_etsin in messages_str, publish_to_etsin) + self.assertEqual(assert_str_ttv in messages_str, publish_to_ttv) + + + + def test_rabbitmq_publish(self): + """ + Creates four different data catalogs and creates, updates, and deletes + catalog records in them. Checks that RabbitMQ messages are published + in correct exchanges with correct routing keys. + """ + param_list = [(True, True), (True, False), (False, True), (False, False)] + for publish_to_etsin, publish_to_ttv in param_list: + with self.subTest(): + + # Create the data catalog + dc = self._get_object_from_test_data("datacatalog", 4) + dc_id = f"urn:nbn:fi:att:data-catalog-att-{publish_to_etsin}-{publish_to_ttv}" + dc["catalog_json"]["identifier"] = dc_id + dc["publish_to_etsin"] = publish_to_etsin + dc["publish_to_ttv"] = publish_to_ttv + dc = self.client.post("/rest/v2/datacatalogs", dc, format="json").data + + # Create the catalog record + cr = self._get_new_full_test_att_cr_data() + cr["data_catalog"] = dc + + cr = self.client.post("/rest/v2/datasets", cr, format="json").data + self._check_rabbitmq_queue("create", publish_to_etsin, publish_to_ttv) + + # Empty the queue + RabbitMQService.messages = [] + + # Update the catalog record + cr["research_dataset"]["description"] = { + "en": "Updating the description" + } + + response = self.client.put(f"/rest/v2/datasets/{cr['id']}", cr, format="json") + self._check_rabbitmq_queue("update", publish_to_etsin, publish_to_ttv) + + RabbitMQService.messages = [] + + # Delete the catalog record + response = self.client.delete(f"/rest/v2/datasets/{cr['id']}") + self._check_rabbitmq_queue("delete", publish_to_etsin, publish_to_ttv) + + RabbitMQService.messages = [] + diff --git a/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py b/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py index ab033762..f2d1230c 100755 --- a/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/dataset_rpc.py @@ -660,3 +660,98 @@ def test_fix_deprecated_nested_directories_3(self): self.assertTrue( "/TestExperiment/Directory_2/Group_2/Group_2_deeper/file_12.txt" not in rd_filepaths ) + +class FlushUserDataTests(CatalogRecordApiWriteCommon): + """ + Tests that datasets created by a certain user can be flushed. + """ + + def test_existing_users_datasets_with_no_files_are_flushed(self): + self._use_http_authorization("metax") + + # create new dataset for user "abcd-1234" + username = "abcd-1234" + self.cr_test_data["metadata_provider_user"] = username + self.cr_test_data["research_dataset"].pop("files", None) + self.cr_test_data["research_dataset"].pop("directories", None) + response = self.client.post( + "/rest/v2/datasets?draft=false", self.cr_test_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + # make sure the dataset exists now + cr = response.data + response = self.client.get("/rest/datasets/{}".format(cr["identifier"]), format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["metadata_provider_user"], username) + + # flush user data for user "abcd-1234" and check that this succeeds + response = self.client.post("/rpc/datasets/flush_user_data?metadata_provider_user=%s" % username) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) + + response = self.client.get("/rest/datasets/{}".format(cr["identifier"]), format="json") + self.assertEqual("not found" in response.json()["detail"].lower(), True) + + def test_existing_users_datasets_with_files_are_flushed(self): + self._use_http_authorization("metax") + + # create new dataset for user "abcde-12345" + username = "abcde-12345" + self.cr_test_data["metadata_provider_user"] = username + response = self.client.post( + "/rest/v2/datasets?draft=false", self.cr_test_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + # make sure the dataset exists now + cr = response.data + response = self.client.get("/rest/datasets/{}".format(cr["identifier"]), format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["metadata_provider_user"], username) + + # save file related to dataset to later make sure it still exists + response = self.client.get("/rest/datasets/{}/files".format(cr["identifier"]), format="json") + cr_file = response.data[0]["identifier"] + + # flush user data for user "abcde-12345" and check that this succeeds + response = self.client.post("/rpc/datasets/flush_user_data?metadata_provider_user=%s" % username) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.data) + + response = self.client.get("/rest/datasets/{}".format(cr["identifier"]), format="json") + self.assertEqual("not found" in response.json()["detail"].lower(), True) + + # check that the file still exists (flushing datasets should not result to files being deleted) + response = self.client.get("/rest/files/{}".format(cr_file), format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_incorrect_parameters(self): + self._use_http_authorization("metax") + + # non existing username should return HTTP 404 + username = "doesnt_exist_123" + response = self.client.post("/rpc/datasets/flush_user_data?metadata_provider_user=%s" % username) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND, response.data) + + # no metadata provider user should return HTTP 400 + response = self.client.post("/rpc/datasets/flush_user_data") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # wrong request method (delete instead of post) + username = "abcde-12345" + self.cr_test_data["metadata_provider_user"] = username + response = self.client.post( + "/rest/v2/datasets?draft=false", self.cr_test_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + response = self.client.delete( + "/rpc/datasets/flush_user_data?metadata_provider_user=%s" % username + ) + self.assertEqual(response.status_code, 501) + + # wrong user + self._use_http_authorization("api_auth_user") + response = self.client.post( + "/rpc/datasets/flush_user_data?metadata_provider_user=%s" % username + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/src/metax_api/tests/api/rpc/base/views/file_rpc.py b/src/metax_api/tests/api/rpc/base/views/file_rpc.py index 1e22da54..a380cb79 100755 --- a/src/metax_api/tests/api/rpc/base/views/file_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/file_rpc.py @@ -50,7 +50,7 @@ def test_wrong_parameters(self): response = self.client.post( "/rpc/files/delete_project?project_identifier=research_project_112" ) - # self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_known_project_identifier(self): response = self.client.post( @@ -78,3 +78,72 @@ def test_datasets_are_marked_deprecated(self): self.client.post("/rpc/files/delete_project?project_identifier=project_x") response = self.client.get("/rest/datasets/%s" % related_dataset.identifier) self.assertEqual(response.data["deprecated"], True) + + +class FlushProjectTests(FileRPCTests): + + """ + Checks that an entire project's files and directories can be deleted. + """ + + def test_wrong_parameters(self): + # correct user, no project identifier + self._use_http_authorization("metax") + response = self.client.post("/rpc/files/flush_project") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # nonexisting project identifier: + response = self.client.post("/rpc/files/flush_project?project_identifier=non_existing") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # wrong request method + response = self.client.delete( + "/rpc/files/flush_project?project_identifier=research_project_112" + ) + self.assertEqual(response.status_code, 501) + + # wrong user + self._use_http_authorization("api_auth_user") + response = self.client.post( + "/rpc/files/flush_project?project_identifier=research_project_112" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_known_project_identifier(self): + self._use_http_authorization("metax") + response = self.client.post( + "/rpc/files/flush_project?project_identifier=research_project_112" + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_files_are_deleted(self): + self._use_http_authorization("metax") + + # make sure project has files before deleting them + files_count_before = File.objects.filter(project_identifier="research_project_112").count() + self.assertNotEqual(files_count_before, 0) + + # delete all files for project + response = self.client.post( + "/rpc/files/flush_project?project_identifier=research_project_112" + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # make sure all files are now deleted + files_count_after = File.objects.filter(project_identifier="research_project_112").count() + self.assertEqual(files_count_after, 0) + + def test_directories_are_deleted(self): + self._use_http_authorization("metax") + + # make sure project has directories before deleting them + dirs_count_before = Directory.objects.filter(project_identifier="research_project_112").count() + self.assertNotEqual(dirs_count_before, 0) + + # delete all directories for project + response = self.client.post("/rpc/files/flush_project?project_identifier=research_project_112") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # make sure all directories are now deleted + dirs_count_after = Directory.objects.filter(project_identifier="research_project_112").count() + self.assertEqual(dirs_count_after, 0) \ No newline at end of file diff --git a/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py b/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py index e330e325..5d57b244 100755 --- a/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py @@ -932,3 +932,61 @@ def test_organization_datasets_cumulative_for_drafts(self): # ensure the totals are calculated without drafts self.assertNotEqual(total_1, total_2, "Count be reduced by one after setting id=1 as draft") + + +class StatisticRPCforOrganizationDatasetsCumulative(StatisticRPCCommon, CatalogRecordApiWriteCommon): + """ + Test suite for organization_datasets_cumulative. Test only optional parameters removed, legacy and latest for now. + """ + + url = "/rpc/statistics/organization_datasets_cumulative" + dateparam_all = "from_date=2018-06-01&to_date=2018-06-30" + + def test_organization_datasets_cumulative_removed(self): + # initially there are 2 datasets + response = self.client.get(f"{self.url}?{self.dateparam_all}").data + self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 2) + + # removed=true should return 0 datasets since none have been removed + response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=true").data + self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 0) + + # remove one dataset from June, so one dataset should be left + self._set_removed_dataset(id=8) + response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=false").data + self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 1) + + # removed=true should return 1 removed dataset now + response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=true").data + self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 1) + + def test_organization_datasets_cumulative_legacy(self): + # let's create 2 legacy datasets for 2018-06 + leg_cr = ( + self._create_legacy_dataset() + ) + self._set_dataset_creation_date(leg_cr, "2018-06-13") + leg_cr2 = ( + self._create_legacy_dataset() + ) + self._set_dataset_creation_date(leg_cr2, "2018-06-13") + + response = self.client.get(f"{self.url}?{self.dateparam_all}&legacy=true").data + self.assertEqual(response["some_org_id"]["urn:nbn:fi:att:data-catalog-legacy"][0]["count"], 2) + + # check that legacy=false returns 0 datasets + response = self.client.get(f"{self.url}?{self.dateparam_all}&legacy=false").data + self.assertEqual(response["some_org_id"]["urn:nbn:fi:att:data-catalog-legacy"][0]["count"], 0) + + def test_organization_datasets_cumulative_latest(self): + # let's create 2 versions of a dataset for 2018-06 + first_version = self._create_new_dataset_version() + self._set_dataset_creation_date(first_version, "2018-06-13") + second_version = self._create_new_dataset_version() + self._set_dataset_creation_date(second_version, "2018-06-13") + response = self.client.get(f"{self.url}?{self.dateparam_all}&latest=false").data # returns all + self.assertEqual(response["org_1"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 2) + + # latest=true should only return the latest version + response = self.client.get(f"{self.url}?{self.dateparam_all}&latest=true").data + self.assertEqual(response["org_1"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 1) \ No newline at end of file diff --git a/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py b/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py index d89f0c29..d61fce67 100755 --- a/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py +++ b/src/metax_api/tests/api/rpc/v2/views/dataset_rpc.py @@ -121,6 +121,21 @@ def test_create_new_version(self): response.data["identifier"], ) + def test_create_new_version_shares_permissions(self): + """ + Ensure new version shares EditorPermissions with the original. + """ + response = self.client.post( + "/rpc/v2/datasets/create_new_version?identifier=5", format="json" + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.data) + + next_version_identifier = response.data.get("identifier") + cr = CR.objects.get(id=5) + next_version_cr = CR.objects.get(identifier=next_version_identifier) + self.assertEqual(cr.editor_permissions_id, next_version_cr.editor_permissions_id) + + def test_delete_new_version_draft(self): """ Ensure a new version that is created into draft state can be deleted, and is permanently deleted. diff --git a/src/metax_api/tests/testdata/data_catalog_test_data_template.json b/src/metax_api/tests/testdata/data_catalog_test_data_template.json index decbf52a..34cc8604 100755 --- a/src/metax_api/tests/testdata/data_catalog_test_data_template.json +++ b/src/metax_api/tests/testdata/data_catalog_test_data_template.json @@ -117,7 +117,7 @@ "dataset_versioning": true }, "service_created": "metax", - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax" + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas" } \ No newline at end of file diff --git a/src/metax_api/tests/testdata/generate_test_data.py b/src/metax_api/tests/testdata/generate_test_data.py index 6729396a..85dcaf78 100755 --- a/src/metax_api/tests/testdata/generate_test_data.py +++ b/src/metax_api/tests/testdata/generate_test_data.py @@ -7,6 +7,7 @@ import os import sys +import uuid from copy import deepcopy from json import dump as json_dump, load as json_load @@ -268,6 +269,7 @@ def save_test_data( contract_list, catalog_record_list, dataset_version_sets, + editor_permissions, ): with open("test_data.json", "w") as f: print("dumping test data as json to metax_api/tests/test_data.json...") @@ -278,6 +280,7 @@ def save_test_data( + data_catalogs_list + contract_list + dataset_version_sets + + editor_permissions + catalog_record_list, f, indent=4, @@ -368,6 +371,30 @@ def generate_contracts(contract_max_rows, validate_json): return test_contract_list +def add_editor_permissions(editor_permissions, dataset): + # add EditorPermissions + pk = len(editor_permissions) + rights_pk = str(uuid.UUID(int=pk)) + editor_perms = { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": rights_pk, + } + editor_permissions.append(editor_perms) + editor_user_perms = { + "fields": { + "user_id": dataset["fields"]["metadata_provider_user"], + "date_created": dataset["fields"]["date_created"], + "editor_permissions_id": rights_pk, + "role": "creator", + }, + "model": "metax_api.editoruserpermission", + "pk": str(uuid.UUID(int=pk + 1)), + } + editor_permissions.append(editor_user_perms) + dataset["fields"]["editor_permissions_id"] = rights_pk + + def generate_catalog_records( basic_catalog_record_max_rows, data_catalogs_list, @@ -377,6 +404,7 @@ def generate_catalog_records( type, test_data_list=[], dataset_version_sets=[], + editor_permissions=[], ): print("generating %s catalog records..." % type) @@ -426,6 +454,8 @@ def generate_catalog_records( new["fields"]["date_created"] = "2017-05-23T10:07:22Z" new["fields"]["files"] = [] + add_editor_permissions(editor_permissions, new) + # add files if type == "ida": @@ -454,62 +484,46 @@ def generate_catalog_records( # first fifth of files dataset_files[-1]["file_type"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/text", - "pref_label": { - "fi": "Teksti", - "en": "Text", - "und": "Teksti" - }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" + "pref_label": {"fi": "Teksti", "en": "Text", "und": "Teksti"}, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", } dataset_files[-1]["use_category"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/source", "pref_label": { "fi": "Lähdeaineisto", "en": "Source material", - "und": "Lähdeaineisto" + "und": "Lähdeaineisto", }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", } elif file_divider <= j < (file_divider * 2): # second fifth of files dataset_files[-1]["file_type"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/video", - "pref_label": { - "fi": "Video", - "en": "Video", - "und": "Video" - }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" + "pref_label": {"fi": "Video", "en": "Video", "und": "Video"}, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", } dataset_files[-1]["use_category"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/outcome", "pref_label": { "fi": "Tulosaineisto", "en": "Outcome material", - "und": "Tulosaineisto" + "und": "Tulosaineisto", }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", } elif (file_divider * 2) <= j < (file_divider * 3): # third fifth of files dataset_files[-1]["file_type"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/file_type/code/image", - "pref_label": { - "fi": "Kuva", - "en": "Image", - "und": "Kuva" - }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" + "pref_label": {"fi": "Kuva", "en": "Image", "und": "Kuva"}, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", } dataset_files[-1]["use_category"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/publication", - "pref_label": { - "fi": "Julkaisu", - "en": "Publication", - "und": "Julkaisu" - }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" + "pref_label": {"fi": "Julkaisu", "en": "Publication", "und": "Julkaisu"}, + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", } elif (file_divider * 3) <= j < (file_divider * 4): # fourth fifth of files @@ -518,18 +532,18 @@ def generate_catalog_records( "pref_label": { "fi": "Lähdekoodi", "en": "Source code", - "und": "Lähdekoodi" + "und": "Lähdekoodi", }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/file_type", } dataset_files[-1]["use_category"] = { "identifier": "http://uri.suomi.fi/codelist/fairdata/use_category/code/documentation", "pref_label": { "fi": "Dokumentaatio", "en": "Documentation", - "und": "Dokumentaatio" + "und": "Dokumentaatio", }, - "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category" + "in_scheme": "http://uri.suomi.fi/codelist/fairdata/use_category", } else: # the rest of files @@ -652,6 +666,8 @@ def generate_catalog_records( new["fields"]["date_modified"] = "2017-09-23T10:07:22Z" new["fields"]["date_created"] = "2017-05-23T10:07:22Z" + add_editor_permissions(editor_permissions, new) + new["fields"]["research_dataset"]["metadata_version_identifier"] = generate_test_identifier( cr_type, len(test_data_list) + 1, urn=False ) @@ -761,7 +777,7 @@ def generate_catalog_records( json_validate(new["fields"]["research_dataset"], json_schema) test_data_list.append(new) - return test_data_list, dataset_version_sets + return test_data_list, dataset_version_sets, editor_permissions def generate_alt_catalog_records(test_data_list): @@ -844,7 +860,7 @@ def set_qvain_info_to_records(catalog_record_list): ida_data_catalog_max_rows + 1, att_data_catalog_max_rows, validate_json, "att" ) - catalog_record_list, dataset_version_sets = generate_catalog_records( + catalog_record_list, dataset_version_sets, editor_permissions = generate_catalog_records( ida_catalog_record_max_rows, ida_data_catalogs_list, contract_list, @@ -853,7 +869,7 @@ def set_qvain_info_to_records(catalog_record_list): "ida", ) - catalog_record_list, dataset_version_sets = generate_catalog_records( + catalog_record_list, dataset_version_sets, editor_permissions = generate_catalog_records( att_catalog_record_max_rows, att_data_catalogs_list, contract_list, @@ -862,6 +878,7 @@ def set_qvain_info_to_records(catalog_record_list): "att", catalog_record_list, dataset_version_sets, + editor_permissions, ) catalog_record_list = generate_alt_catalog_records(catalog_record_list) @@ -876,6 +893,7 @@ def set_qvain_info_to_records(catalog_record_list): contract_list, catalog_record_list, dataset_version_sets, + editor_permissions, ) print("done") diff --git a/src/metax_api/tests/testdata/test_data.json b/src/metax_api/tests/testdata/test_data.json index c3802a10..025b175e 100755 --- a/src/metax_api/tests/testdata/test_data.json +++ b/src/metax_api/tests/testdata/test_data.json @@ -4537,9 +4537,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -4667,9 +4667,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -4797,9 +4797,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -4927,9 +4927,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -5057,9 +5057,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -5187,9 +5187,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -5317,9 +5317,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -5447,9 +5447,9 @@ "fi": "Testidatakatalogin nimi" } }, - "catalog_record_services_create": "testuser,api_auth_user,metax", - "catalog_record_services_edit": "testuser,api_auth_user,metax", - "catalog_record_services_read": "testuser,api_auth_user,metax", + "catalog_record_services_create": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_edit": "testuser,api_auth_user,metax,tpas", + "catalog_record_services_read": "testuser,api_auth_user,metax,tpas", "date_created": "2017-05-15T10:07:22Z", "date_modified": "2017-06-15T10:07:22Z", "service_created": "metax" @@ -5712,6 +5712,396 @@ "model": "metax_api.datasetversionset", "pk": 13 }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000000" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000000", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000001" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000002" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000002", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000003" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000004" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000004", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000005" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000006" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000006", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000007" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000008" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000008", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000009" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000000a" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000a", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000000b" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000000c" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000c", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000000d" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000000e" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000e", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000000f" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000010" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000010", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000011" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000012" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000012", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000013" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000014" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000014", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000015" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000016" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000016", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000017" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000018" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000018", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000019" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000001a" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001a", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000001b" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000001c" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001c", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000001d" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000001e" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001e", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000001f" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000020" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000020", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000021" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000022" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000022", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000023" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000024" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000024", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000025" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000026" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000026", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000027" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000028" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000028", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000029" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000002a" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002a", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000002b" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000002c" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002c", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000002d" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-00000000002e" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002e", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-00000000002f" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000030" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000030", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000031" + }, + { + "fields": {}, + "model": "metax_api.editorpermissions", + "pk": "00000000-0000-0000-0000-000000000032" + }, + { + "fields": { + "date_created": "2017-05-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000032", + "role": "creator", + "user_id": "abc-user-123" + }, + "model": "metax_api.editoruserpermission", + "pk": "00000000-0000-0000-0000-000000000033" + }, { "fields": {}, "model": "metax_api.alternaterecordset", @@ -5727,6 +6117,7 @@ "dataset_version_set": 1, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000000", "files": [ 1, 2 @@ -5882,6 +6273,7 @@ "dataset_version_set": 2, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000002", "files": [ 3, 4 @@ -6037,6 +6429,7 @@ "dataset_version_set": 3, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000004", "files": [ 5, 6 @@ -6192,6 +6585,7 @@ "dataset_version_set": 4, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000006", "files": [ 7, 8 @@ -6347,6 +6741,7 @@ "dataset_version_set": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000008", "files": [ 9, 10 @@ -6502,6 +6897,7 @@ "dataset_version_set": 6, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000a", "files": [ 11, 12 @@ -6657,6 +7053,7 @@ "dataset_version_set": 7, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000c", "files": [ 13, 14 @@ -6812,6 +7209,7 @@ "dataset_version_set": 8, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000000e", "files": [ 15, 16 @@ -6952,6 +7350,7 @@ "dataset_version_set": 9, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000010", "files": [ 17, 18 @@ -7078,6 +7477,7 @@ "dataset_version_set": 10, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000012", "files": [ 19, 20 @@ -7201,6 +7601,7 @@ "dataset_version_set": 11, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000014", "files": [ 1, 2, @@ -8152,6 +8553,7 @@ "dataset_version_set": 12, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000016", "files": [ 1, 2, @@ -9103,6 +9505,7 @@ "dataset_version_set": 13, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000018", "files": [ 22, 23, @@ -10146,6 +10549,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001a", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9614", "metadata_owner_org": "abc-org-123", @@ -10285,6 +10689,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001c", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9615", "metadata_owner_org": "abc-org-123", @@ -10424,6 +10829,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000001e", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9616", "metadata_owner_org": "abc-org-123", @@ -10563,6 +10969,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000020", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9617", "metadata_owner_org": "abc-org-123", @@ -10702,6 +11109,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000022", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9618", "metadata_owner_org": "abc-org-123", @@ -10841,6 +11249,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000024", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9619", "metadata_owner_org": "abc-org-123", @@ -10980,6 +11389,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000026", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9620", "metadata_owner_org": "abc-org-123", @@ -11110,6 +11520,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000028", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9621", "metadata_owner_org": "abc-org-123", @@ -11240,6 +11651,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002a", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9622", "metadata_owner_org": "abc-org-123", @@ -11370,6 +11782,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002c", "files": [], "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9623", "metadata_owner_org": "abc-org-123", @@ -11499,6 +11912,7 @@ "data_catalog": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-00000000002e", "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9624", "metadata_owner_org": "abc-org-123", "metadata_provider_org": "abc-org-123", @@ -12433,6 +12847,7 @@ "data_catalog": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000030", "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9625", "metadata_owner_org": "abc-org-123", "metadata_provider_org": "abc-org-123", @@ -13367,6 +13782,7 @@ "data_catalog": 5, "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-09-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000032", "identifier": "cr955e904-e3dd-4d7e-99f1-3fed446f9626", "metadata_owner_org": "abc-org-123", "metadata_provider_org": "abc-org-123", @@ -14304,6 +14720,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000012", "files": [ 19, 20 @@ -14429,6 +14846,7 @@ "dataset_group_edit": "default-dataset-edit-group", "date_created": "2017-05-23T10:07:22Z", "date_modified": "2017-06-23T10:07:22Z", + "editor_permissions_id": "00000000-0000-0000-0000-000000000012", "files": [ 19, 20 From da70dc59c7eeb1e69a392804f76a5436f69ff89e Mon Sep 17 00:00:00 2001 From: Toni Nurmi Date: Fri, 11 Mar 2022 15:31:47 +0000 Subject: [PATCH 20/21] 22/03/Hotfix/2 Production Update --- .gitlab-ci.yml | 4 +- .../api/rpc/base/views/statistic_rpc.py | 28 ++++++++++ .../commands/create_statistic_report.py | 47 ++++++++++++++++ .../management/commands/update_orgs.py | 19 +++++-- .../migrations/0047_update_vrk_datasets.py | 55 +++++++++++++++++++ ...rganizationstatistics_projectstatistics.py | 30 ++++++++++ src/metax_api/models/__init__.py | 2 + src/metax_api/models/catalog_record.py | 9 ++- .../models/organization_statistics.py | 18 ++++++ src/metax_api/models/project_statistics.py | 18 ++++++ src/metax_api/onappstart.py | 1 + src/metax_api/services/file_service.py | 9 ++- src/metax_api/services/statistic_service.py | 27 ++++++++- .../settings/components/access_control.py | 2 + src/metax_api/swagger/v1/swagger.yaml | 30 ++++++++++ src/metax_api/swagger/v2/swagger.yaml | 30 ++++++++++ .../resources/organizations/organizations.csv | 2 +- .../tests/api/rpc/base/views/statistic_rpc.py | 26 +++++---- .../commands/statistics_summaries.py | 39 +++++++++++++ 19 files changed, 371 insertions(+), 25 deletions(-) create mode 100644 src/metax_api/management/commands/create_statistic_report.py create mode 100644 src/metax_api/migrations/0047_update_vrk_datasets.py create mode 100644 src/metax_api/migrations/0048_organizationstatistics_projectstatistics.py create mode 100644 src/metax_api/models/organization_statistics.py create mode 100644 src/metax_api/models/project_statistics.py create mode 100644 src/metax_api/tests/management/commands/statistics_summaries.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index faafdcb6..7a31fe95 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,8 +46,8 @@ clean_gitlab_env: - echo "Cleaning deleted branches from environments" rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - when: manual - - when: never + when: never + - when: manual update_metax: stage: update diff --git a/src/metax_api/api/rpc/base/views/statistic_rpc.py b/src/metax_api/api/rpc/base/views/statistic_rpc.py index aa7479d1..9a432884 100755 --- a/src/metax_api/api/rpc/base/views/statistic_rpc.py +++ b/src/metax_api/api/rpc/base/views/statistic_rpc.py @@ -153,3 +153,31 @@ def count_files(self, request): } return Response(StatisticService.count_files(**params)) + + @action(detail=False, methods=["get"], url_path="projects_summary") + def projects_summary(self, request): + if not request.query_params.get("projects"): + params = { + "projects": None, + } + + else: + params = { + "projects": list(CS.get_list_query_param(request, "projects")), + } + + return Response(StatisticService.projects_summary(**params)) + + @action(detail=False, methods=["get"], url_path="organizations_summary") + def organizations_summary(self, request): + if not request.query_params.get("organizations"): + params = { + "organizations": None, + } + + else: + params = { + "organizations": list(CS.get_list_query_param(request, "organizations")), + } + + return Response(StatisticService.organizations_summary(**params)) diff --git a/src/metax_api/management/commands/create_statistic_report.py b/src/metax_api/management/commands/create_statistic_report.py new file mode 100644 index 00000000..a68d8021 --- /dev/null +++ b/src/metax_api/management/commands/create_statistic_report.py @@ -0,0 +1,47 @@ +import logging + +from django.core.management.base import BaseCommand +from django.db.models import Sum +from django.db.models.expressions import RawSQL + +from metax_api.models import File, CatalogRecordV2, OrganizationStatistics, ProjectStatistics +from metax_api.api.rest.base.views import FileViewSet +from metax_api.services import FileService, StatisticService + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + def handle(self, *args, **options): + + logger.info("Creating statistic summary") + + OrganizationStatistics.objects.all().delete() + ProjectStatistics.objects.all().delete() + + + ida_projects = File.objects.all().values("project_identifier").distinct() + for project in ida_projects: + project_id = project["project_identifier"] + ret = StatisticService.count_files([project_id], include_pids=True) + count = ret[0]["count"] + size = ret[0]["byte_size"] + file_pids = ret[1] + + if len(file_pids) == 0: + catalog_records = "" + else: + catalog_records = FileService.get_identifiers(file_pids, "noparams", True, get_pids=True).data + + stat = ProjectStatistics(project_id, count, size, catalog_records) + stat.save() + + + organizations = CatalogRecordV2.objects.all().order_by().values("metadata_provider_org").distinct() + + for org in organizations: + org_id = org["metadata_provider_org"] + ret = StatisticService.count_datasets(metadata_provider_org=org_id) + stat = OrganizationStatistics(org_id, ret["count"], ret["ida_byte_size"]) + stat.save() + + logger.info("Statistic summary created") \ No newline at end of file diff --git a/src/metax_api/management/commands/update_orgs.py b/src/metax_api/management/commands/update_orgs.py index fb81d844..adce895e 100644 --- a/src/metax_api/management/commands/update_orgs.py +++ b/src/metax_api/management/commands/update_orgs.py @@ -107,7 +107,10 @@ def get_orgs_from_api() -> List[Organization]: orgs.append(o) o = Organization(name_fi, name_en, org_code) + if name_sv: + o.org_name_sv = name_sv orgs.append(o) + logger.info(f"retrieved {len(orgs)} organizations from research.fi") return orgs @@ -153,9 +156,12 @@ def handle(self, *args, **options): added += 1 logger.info(f"Added {added} organisations from research.fi to local org list") + # sort orgs alphabetically s = sorted(union, key=lambda i: (i.org_name_fi, i.unit_name)) - with open(settings.ORG_FILE_PATH, "w") as f: + # write new orgs to local csv + with open(settings.ORG_FILE_PATH, "w", newline='') as f: logger.info("writing updated csv") + # remove duplicates no_duplicates = [] for c in s: if c not in no_duplicates: @@ -165,6 +171,7 @@ def handle(self, *args, **options): writer = csv.DictWriter( f, fieldnames=CSV_HEADERS, + lineterminator='\n', ) writer.writeheader() for i in csv_serialized: @@ -174,9 +181,11 @@ def handle(self, *args, **options): # Malformed values from TTV api if i["unit_name"] == "LÄÄKETIETEELLINEN TIEDEKUNTA": continue - if "Ã…bo" in i["unit_name"]: - i["unit_name"] = str(i["unit_name"]).replace("Ã…bo", "Åbo") - if "ÖS" in i["unit_name"]: - i["unit_name"] = str(i["unit_name"]).replace("ÖS", "Ö") + if "Ã…" in i["unit_name"]: + i["unit_name"] = str(i["unit_name"]).replace("Ã…", "Å") + if "Ö" in i["unit_name"]: + i["unit_name"] = str(i["unit_name"]).replace("Ö", "Ö") + if "Ä" in i["unit_name"]: + i["unit_name"] = str(i["unit_name"]).replace("Ä", "Ä") writer.writerow(i) logger.info("successfully updated organization csv") diff --git a/src/metax_api/migrations/0047_update_vrk_datasets.py b/src/metax_api/migrations/0047_update_vrk_datasets.py new file mode 100644 index 00000000..14b2a65b --- /dev/null +++ b/src/metax_api/migrations/0047_update_vrk_datasets.py @@ -0,0 +1,55 @@ +from django.db import migrations + +import logging + +logger = logging.getLogger(__name__) + +def update_vrk_datasets(apps, schema_editor): + + logger.info("Updating organization info of catalog records by Väestörekisterikeskus") + + CatalogRecord = apps.get_model('metax_api', 'CatalogRecord') + new_name_fi = "Digi- ja väestötietovirasto" + new_name_en = "Digital and Population Data Services Agency" + new_name_sv = "Myndigheten för digitalisering och befolkningsdata" + description_suffix_fi = "\n\nAineiston luojaorganisaation aikaisempi nimi: Väestörekisterikeskus." + description_suffix_en = "\n\nPrevious name of dataset creator organization: Population Register Center." + + # Catalog Records by Väestörekisterikeskus + # Getting these from the database using Django filters would have + # been too complicated, so instead they are hardcoded + cr_ids = [ + "a3610de8-73fa-4e25-a89b-320549c71f0a", + "b77c91cf-a437-4d01-b2ec-efb08605d559", + "7787c312-3973-4e16-a032-7b89a0257739" + ] + + crs = CatalogRecord.objects.filter(identifier__in = cr_ids) + logger.info(f"Found {len(crs)} catalog records to update") + for cr in crs: + cr_json = cr.research_dataset + logger.info(f"Updating catalog record: {cr}") + cr_json["creator"][0]["name"]["en"] = new_name_en + cr_json["creator"][0]["name"]["fi"] = new_name_fi + cr_json["creator"][0]["name"]["sv"] = new_name_sv + if description_suffix_en not in cr_json["description"]["en"]: + cr_json["description"]["en"] += description_suffix_en + if description_suffix_fi not in cr_json["description"]["fi"]: + cr_json["description"]["fi"] += description_suffix_fi + cr.save() + + +def revert(apps, schema_editor): + pass + + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0046_replace_dataset_owner'), + ] + + operations = [ + migrations.RunPython(update_vrk_datasets, revert), + ] diff --git a/src/metax_api/migrations/0048_organizationstatistics_projectstatistics.py b/src/metax_api/migrations/0048_organizationstatistics_projectstatistics.py new file mode 100644 index 00000000..d130481a --- /dev/null +++ b/src/metax_api/migrations/0048_organizationstatistics_projectstatistics.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.10 on 2021-12-31 07:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('metax_api', '0047_update_vrk_datasets'), + ] + + operations = [ + migrations.CreateModel( + name='OrganizationStatistics', + fields=[ + ('organization', models.CharField(max_length=200, primary_key=True, serialize=False)), + ('count', models.IntegerField()), + ('byte_size', models.IntegerField()), + ], + ), + migrations.CreateModel( + name='ProjectStatistics', + fields=[ + ('project_identifier', models.CharField(max_length=200, primary_key=True, serialize=False)), + ('count', models.IntegerField()), + ('byte_size', models.IntegerField()), + ('published_datasets', models.TextField()), + ], + ), + ] diff --git a/src/metax_api/models/__init__.py b/src/metax_api/models/__init__.py index 643cac39..92457f2e 100755 --- a/src/metax_api/models/__init__.py +++ b/src/metax_api/models/__init__.py @@ -16,4 +16,6 @@ from .file import File from .file_storage import FileStorage from .metax_user import MetaxUser +from .organization_statistics import OrganizationStatistics +from .project_statistics import ProjectStatistics from .xml_metadata import XmlMetadata diff --git a/src/metax_api/models/catalog_record.py b/src/metax_api/models/catalog_record.py index b30a9eea..dd9561b0 100755 --- a/src/metax_api/models/catalog_record.py +++ b/src/metax_api/models/catalog_record.py @@ -611,7 +611,7 @@ def user_has_access(self, request): raise Http404 # write operation - return self.user_is_owner(request) + return self.user_is_privileged(request) def user_is_owner(self, request): if self.state == self.STATE_DRAFT and self.metadata_provider_user != request.user.username: @@ -648,11 +648,14 @@ def user_is_privileged(self, request): return True else: return True + + users = self.editor_permissions.users + ids = users.all().values_list('user_id', flat=True) + if request.user.username in ids: + return True elif self.user_is_owner(request): - # can see sensitive fields return True else: - # unknown user return False def _check_catalog_permissions(self, catalog_groups, catalog_services, request=None): diff --git a/src/metax_api/models/organization_statistics.py b/src/metax_api/models/organization_statistics.py new file mode 100644 index 00000000..13fc2688 --- /dev/null +++ b/src/metax_api/models/organization_statistics.py @@ -0,0 +1,18 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT + +import logging + +from django.db import models + +_logger = logging.getLogger(__name__) + +class OrganizationStatistics(models.Model): + organization = models.CharField(primary_key=True, max_length=200) + count = models.IntegerField() + byte_size = models.IntegerField() + diff --git a/src/metax_api/models/project_statistics.py b/src/metax_api/models/project_statistics.py new file mode 100644 index 00000000..813e76d8 --- /dev/null +++ b/src/metax_api/models/project_statistics.py @@ -0,0 +1,18 @@ +# This file is part of the Metax API service +# +# Copyright 2017-2018 Ministry of Education and Culture, Finland +# +# :author: CSC - IT Center for Science Ltd., Espoo Finland +# :license: MIT + +import logging + +from django.db import models + +_logger = logging.getLogger(__name__) + +class ProjectStatistics(models.Model): + project_identifier = models.CharField(primary_key=True, max_length=200) + count = models.IntegerField() + byte_size = models.IntegerField() + published_datasets = models.TextField() \ No newline at end of file diff --git a/src/metax_api/onappstart.py b/src/metax_api/onappstart.py index a54b4a61..98ace44d 100755 --- a/src/metax_api/onappstart.py +++ b/src/metax_api/onappstart.py @@ -15,6 +15,7 @@ from metax_api.utils import ReferenceDataLoader, executing_test_case, convert_yaml_to_html + _logger = logging.getLogger(__name__) diff --git a/src/metax_api/services/file_service.py b/src/metax_api/services/file_service.py index ad0c983e..6b8bfdac 100755 --- a/src/metax_api/services/file_service.py +++ b/src/metax_api/services/file_service.py @@ -232,7 +232,7 @@ def restore_files(cls, request, file_identifier_list): return Response({"restored_files_count": affected_rows}, status=status.HTTP_200_OK) @classmethod - def get_identifiers(cls, identifiers, params, keysonly): + def get_identifiers(cls, identifiers, params, keysonly, get_pids = False): """ keys='files': Find out which (non-deprecated) datasets a list of files belongs to, and return their preferred_identifiers per file as a list in json format. @@ -243,6 +243,8 @@ def get_identifiers(cls, identifiers, params, keysonly): keysonly= for dataset return dataset ids that have files, for files return file ids that belong to some dataset + get_pids: get preferred identifiers instead of identifiers. Only applicable, if params = "noparams" + Parameter identifiers can be a list of pk's (integers), or file/dataset identifiers (strings). """ _logger.info("Retrieving detailed list of %s" % params) @@ -253,7 +255,7 @@ def get_identifiers(cls, identifiers, params, keysonly): _logger.info( "Searching return for the following %s (printing first 10):\n%s" - % (params, "\n".join(str(id) for id in ids[:10])) + % (params, ", ".join(str(id) for id in ids[:10])) ) noparams = """ @@ -266,6 +268,9 @@ def get_identifiers(cls, identifiers, params, keysonly): GROUP BY cr.identifier """ + if get_pids: + noparams = noparams.replace("cr.identifier", "research_dataset->>'preferred_identifier'") + files = """ SELECT f.identifier, json_agg(cr.identifier) FROM metax_api_file f diff --git a/src/metax_api/services/statistic_service.py b/src/metax_api/services/statistic_service.py index ac052619..4e85c9e4 100755 --- a/src/metax_api/services/statistic_service.py +++ b/src/metax_api/services/statistic_service.py @@ -13,7 +13,7 @@ from django.db.models.functions import Coalesce from metax_api.exceptions import Http400 -from metax_api.models import CatalogRecord, DataCatalog, File +from metax_api.models import CatalogRecord, DataCatalog, File, ProjectStatistics, OrganizationStatistics _logger = logging.getLogger(__name__) @@ -616,7 +616,7 @@ def unused_files(cls): return file_stats @classmethod - def count_files(cls, projects, removed=None): + def count_files(cls, projects, removed=None, include_pids=False): kwargs = OrderedDict() file_query = File.objects_unfiltered.all() @@ -630,4 +630,27 @@ def count_files(cls, projects, removed=None): .distinct() # Coalesce is required to provides default value + if include_pids: + return file_query.aggregate(count=Count("id"), byte_size=Coalesce(Sum("byte_size"), 0)), list(file_query.values_list('identifier', flat=True)) return file_query.aggregate(count=Count("id"), byte_size=Coalesce(Sum("byte_size"), 0)) + + @classmethod + def projects_summary(cls, projects): + stats_query = ProjectStatistics.objects.all() + if not projects is None: + stats_query = stats_query.filter(project_identifier__in=projects) + summary = stats_query.values() + if len(summary) == 0: + summary = f"No projects found with project_identifier: {projects}" + return summary + + @classmethod + def organizations_summary(cls, organizations): + stats_query = OrganizationStatistics.objects.all() + if not organizations is None: + stats_query = stats_query.filter(organization__in=organizations) + summary = stats_query.values() + if len(summary) == 0: + summary = f"No organizations found with organization_identifier: {organizations}" + return summary + diff --git a/src/metax_api/settings/components/access_control.py b/src/metax_api/settings/components/access_control.py index ebd99fd4..fab7feaa 100755 --- a/src/metax_api/settings/components/access_control.py +++ b/src/metax_api/settings/components/access_control.py @@ -186,6 +186,8 @@ def __lt__(self, other): api_permissions.rpc.statistics.harvested_datasets_cumulative.use = [Role.ALL] api_permissions.rpc.statistics.organization_datasets_cumulative.use = [Role.ALL] api_permissions.rpc.statistics.unused_files.use = [Role.ALL] +api_permissions.rpc.statistics.projects_summary.use = [Role.ALL] +api_permissions.rpc.statistics.organizations_summary.use = [Role.ALL] def prepare_perm_values(d): diff --git a/src/metax_api/swagger/v1/swagger.yaml b/src/metax_api/swagger/v1/swagger.yaml index 724065b8..49c82192 100755 --- a/src/metax_api/swagger/v1/swagger.yaml +++ b/src/metax_api/swagger/v1/swagger.yaml @@ -2002,6 +2002,36 @@ paths: # $ref: "#/examples/unused_files" # tags: # - Statistics RPC + /rpc/statistics/projects_summary: + get: + summary: Get summary statistics of all projects or a single project. + description: Retrieve dataset count, total byte size, and preferred identifiers of the published datasets in a project. + parameters: + - name: projects + in: query + description: A Comma-separated list of project identifiers. If left empty, the response will include every project in the database. + required: false + type: string + responses: + '200': + description: Successful operation + tags: + - Statistics RPC + /rpc/statistics/organizations_summary: + get: + summary: Get summary statistics of all organizations or a single project. + description: Retrieve dataset count and total byte size of the published datasets by an organization. + parameters: + - name: organizations + in: query + description: A Comma-separated list of organizations. If left empty, the response will include every organization in the database. + required: false + type: string + responses: + '200': + description: Successful operation + tags: + - Statistics RPC /rpc/files/delete_project: post: summary: Deletes given project from the database. diff --git a/src/metax_api/swagger/v2/swagger.yaml b/src/metax_api/swagger/v2/swagger.yaml index 24560201..606c7ed6 100755 --- a/src/metax_api/swagger/v2/swagger.yaml +++ b/src/metax_api/swagger/v2/swagger.yaml @@ -2287,6 +2287,36 @@ paths: # $ref: "#/examples/unused_files" # tags: # - Statistics RPC + /rpc/v2/statistics/projects_summary: + get: + summary: Get summary statistics of all projects or a single project. + description: Retrieve dataset count, total byte size, and preferred identifiers of the published datasets in a project. + parameters: + - name: projects + in: query + description: A Comma-separated list of project identifiers. If left empty, the response will include every project in the database. + required: false + type: string + responses: + '200': + description: Successful operation + tags: + - Statistics RPC + /rpc/v2/statistics/organizations_summary: + get: + summary: Get summary statistics of all organizations or a single project. + description: Retrieve dataset count and total byte size of the published datasets by an organization. + parameters: + - name: organizations + in: query + description: A Comma-separated list of organizations. If left empty, the response will include every organization in the database. + required: false + type: string + responses: + '200': + description: Successful operation + tags: + - Statistics RPC /rpc/v2/files/delete_project: post: summary: Deletes given project from the database. diff --git a/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv b/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv index 0442f975..0c412f4a 100755 --- a/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv +++ b/src/metax_api/tasks/refdata/refdata_indexer/resources/organizations/organizations.csv @@ -137,6 +137,7 @@ Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-amm Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,64,Talouspalvelut,, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,97,Tulkkaustutkinnot ja kielet,, Diakonia-ammattikorkeakoulu,Diaconia University of Applied Sciences,Diakonia-ammattikorkeakoulu,02623,,62,"Viestinta, tiedotus ja markkinointi",, +Digi- ja väestötietovirasto,Digital and Population Data Services Agency,Myndigheten för digitalisering och befolkningsdata,02454372,,,,, Geologian tutkimuskeskus,Geological Survey of Finland,,5040011,,,,, Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030014,Alueellinen geotieto ALG,, Geologian tutkimuskeskus,Geological Survey of Finland,Geologiska forskningscentralen,5040011,,504030016,Digitaaliset tuotteet ja palvelut DIP,, @@ -3605,7 +3606,6 @@ Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,279000,Yliopiston y Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,2790050,Yliopiston yhteiset,, Vaasan yliopisto,University of Vaasa,Vasa universitet,01913,,270800,Yliopistopalvelut,, Valtion taloudellinen tutkimuskeskus,VATT Institute for Economic Research,,3060016,,,,, -Väestörekisterikeskus,Population Register Center,,02454372,,,,, Väylävirasto,Finnish Transport Infrastructure Agency,,10105471,,,,, Yrkeshögskolan Arcada,Arcada University of Applied Sciences,,02535,,,,, Yrkeshögskolan Arcada,Arcada University of Applied Sciences,Yrkeshögskolan Arcada,02535,,111,Alla utbildningar på hälsa och välfärd föruto,, diff --git a/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py b/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py index 5d57b244..4b26e268 100755 --- a/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py +++ b/src/metax_api/tests/api/rpc/base/views/statistic_rpc.py @@ -9,6 +9,7 @@ from django.conf import settings from django.core.management import call_command + from rest_framework import status from rest_framework.test import APITestCase @@ -943,22 +944,27 @@ class StatisticRPCforOrganizationDatasetsCumulative(StatisticRPCCommon, CatalogR dateparam_all = "from_date=2018-06-01&to_date=2018-06-30" def test_organization_datasets_cumulative_removed(self): + catalog_id = "urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1" + catalog = DataCatalog.objects.get(catalog_json__identifier=catalog_id) # initially there are 2 datasets - response = self.client.get(f"{self.url}?{self.dateparam_all}").data - self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 2) + initial_count = CatalogRecord.objects.filter(data_catalog=catalog).count() # removed=true should return 0 datasets since none have been removed response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=true").data - self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 0) + self.assertEqual(response["org_2"][catalog_id][0]["count"], 0) - # remove one dataset from June, so one dataset should be left - self._set_removed_dataset(id=8) - response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=false").data - self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 1) + # remove one dataset + cr = CatalogRecord.objects.filter(data_catalog_id=catalog.id).first() + cr.removed = True + cr.force_save() + + crs_count_after_remove = CatalogRecord.objects.filter(data_catalog_id=catalog.id).count() + + removed_count = CatalogRecord.objects_unfiltered.filter(data_catalog_id=catalog.id, removed=True).count() + + self.assertEqual(crs_count_after_remove + 1, initial_count) + self.assertEqual(removed_count, 1) - # removed=true should return 1 removed dataset now - response = self.client.get(f"{self.url}?{self.dateparam_all}&removed=true").data - self.assertEqual(response["org_2"]["urn:nbn:fi:att:2955e904-e3dd-4d7e-99f1-3fed446f96d1"][0]["count"], 1) def test_organization_datasets_cumulative_legacy(self): # let's create 2 legacy datasets for 2018-06 diff --git a/src/metax_api/tests/management/commands/statistics_summaries.py b/src/metax_api/tests/management/commands/statistics_summaries.py new file mode 100644 index 00000000..1dc87be8 --- /dev/null +++ b/src/metax_api/tests/management/commands/statistics_summaries.py @@ -0,0 +1,39 @@ +import logging + +from django.core.management import call_command +from django.test import TestCase + +from metax_api.models import OrganizationStatistics, ProjectStatistics +from metax_api.tests.utils import test_data_file_path + +_logger = logging.getLogger(__name__) + +class StatisticsSummariesTest(TestCase): + @classmethod + def setUpClass(cls): + """ + Loaded only once for test cases inside this class. + """ + call_command("loaddata", test_data_file_path, verbosity=0) + call_command("create_statistic_report") + + super(StatisticsSummariesTest, cls).setUpClass() + + + def testProjectStatistics(self): + project_x_stats = ProjectStatistics.objects.get(project_identifier="project_x") + research_project_112_stats = ProjectStatistics.objects.get(project_identifier="research_project_112") + + self.assertEqual(project_x_stats.count, 20) + self.assertEqual(project_x_stats.byte_size, 21000) + self.assertEqual(len(project_x_stats.published_datasets.split(",")), 20) + + self.assertEqual(research_project_112_stats.count, 88) + self.assertEqual(research_project_112_stats.byte_size, 625000) + self.assertEqual(len(research_project_112_stats.published_datasets.split(",")), 88) + + def testOrganizationStatistics(self): + org_stats = OrganizationStatistics.objects.get(organization="abc-org-123") + + self.assertEqual(org_stats.count, 28) + self.assertEqual(org_stats.byte_size, 710800) \ No newline at end of file From 6a1780b19d2d70a8b53612ec4aaf641f360153db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Sep 2022 12:39:46 +0000 Subject: [PATCH 21/21] Bump pyyaml from 5.4.1 to 6.0 Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.4.1 to 6.0. - [Release notes](https://github.com/yaml/pyyaml/releases) - [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES) - [Commits](https://github.com/yaml/pyyaml/compare/5.4.1...6.0) --- updated-dependencies: - dependency-name: pyyaml dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- poetry.lock | 1096 +++++++++++++++++++++++------------------------- pyproject.toml | 6 +- 2 files changed, 519 insertions(+), 583 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8be0ea6c..2a54e759 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,7 +16,7 @@ python-versions = "*" [[package]] name = "appnope" -version = "0.1.2" +version = "0.1.3" description = "Disable App Nap on macOS >= 10.9" category = "dev" optional = false @@ -24,17 +24,17 @@ python-versions = "*" [[package]] name = "asgiref" -version = "3.4.1" +version = "3.5.2" description = "ASGI specs, helper code, and adapters" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "asttokens" @@ -52,36 +52,25 @@ test = ["astroid", "pytest"] [[package]] name = "attrs" -version = "21.2.0" +version = "21.4.0" description = "Classes Without Boilerplate" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] - -[[package]] -name = "autosemver" -version = "0.5.5" -description = "Tools to handle automatic semantic versioning in python" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -dulwich = ">=0.19.6,<0.20" +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] [[package]] name = "babel" -version = "2.9.1" +version = "2.10.3" description = "Internationalization utilities" category = "main" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] pytz = ">=2015.7" @@ -118,30 +107,30 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "certifi" -version = "2021.10.8" +version = "2022.6.15" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "charset-normalizer" -version = "2.0.9" +version = "2.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.5.0" +python-versions = ">=3.6.0" [package.extras] unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "8.0.3" +version = "8.1.3" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -149,7 +138,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "main" optional = false @@ -184,13 +173,13 @@ lxml = ">=4.5.0" requests = ">=2.5.0" [package.extras] -all = ["Sphinx (>=3)", "responses (>=0.10.6)", "mock (>=1.3.0)", "pytest-invenio (>=1.4.0)"] +all = ["Sphinx (>=3)", "mock (>=1.3.0)", "pytest-invenio (>=1.4.0)", "responses (>=0.10.6)"] docs = ["Sphinx (>=3)"] -tests = ["responses (>=0.10.6)", "mock (>=1.3.0)", "pytest-invenio (>=1.4.0)"] +tests = ["mock (>=1.3.0)", "pytest-invenio (>=1.4.0)", "responses (>=0.10.6)"] [[package]] name = "decorator" -version = "5.1.0" +version = "5.1.1" description = "Decorators for Humans" category = "dev" optional = false @@ -198,7 +187,7 @@ python-versions = ">=3.5" [[package]] name = "django" -version = "3.2.10" +version = "3.2.14" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -215,14 +204,14 @@ bcrypt = ["bcrypt"] [[package]] name = "django-debug-toolbar" -version = "3.2.3" +version = "3.5.0" description = "A configurable set of panels that display various debug information about the current request/response." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -Django = ">=2.2" +Django = ">=3.2" sqlparse = ">=0.2.0" [[package]] @@ -243,7 +232,7 @@ python-versions = ">=3.6,<4.0" [[package]] name = "django-watchman" -version = "1.2.0" +version = "1.3.0" description = "django-watchman exposes a status endpoint for your backing services" category = "dev" optional = false @@ -254,7 +243,7 @@ django = ">=2.0" [[package]] name = "djangorestframework" -version = "3.13.0" +version = "3.13.1" description = "Web APIs for Django, made easy." category = "main" optional = false @@ -272,26 +261,9 @@ category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[[package]] -name = "dulwich" -version = "0.19.16" -description = "Python Git Library" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -certifi = "*" -urllib3 = ">=1.24.1" - -[package.extras] -fastimport = ["fastimport"] -pgp = ["gpg"] -https = ["urllib3[secure] (>=1.24.1)"] - [[package]] name = "elasticsearch" -version = "7.16.1" +version = "7.17.4" description = "Python client for Elasticsearch" category = "main" optional = false @@ -303,13 +275,13 @@ urllib3 = ">=1.21.1,<2" [package.extras] async = ["aiohttp (>=3,<4)"] -develop = ["requests (>=2.0.0,<3.0.0)", "coverage", "mock", "pyyaml", "pytest", "pytest-cov", "sphinx (<1.7)", "sphinx-rtd-theme", "black", "jinja2"] +develop = ["black", "coverage", "jinja2", "mock", "pytest", "pytest-cov", "pyyaml", "requests (>=2.0.0,<3.0.0)", "sphinx (<1.7)", "sphinx-rtd-theme"] docs = ["sphinx (<1.7)", "sphinx-rtd-theme"] requests = ["requests (>=2.4.0,<3.0.0)"] [[package]] name = "executing" -version = "0.8.2" +version = "0.8.3" description = "Get the currently executing AST node of a frame, and other information" category = "dev" optional = false @@ -331,7 +303,7 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "icecream" -version = "2.1.1" +version = "2.1.3" description = "Never use print() to debug again; inspect variables, expressions, and program execution with a single, simple function call." category = "dev" optional = false @@ -353,24 +325,24 @@ python-versions = ">=3.5" [[package]] name = "idutils" -version = "1.1.9" +version = "1.1.12" description = "Small library for persistent identifiers used in scholarly communication." category = "main" optional = false python-versions = "*" [package.dependencies] -isbnid-fork = ">=0.4.4" +isbnlib = ">=3.10.8" six = ">=1.10" [package.extras] -all = ["Sphinx (>=3)", "pytest-cache (>=1.0)", "pytest-runner (>=2.6.2)", "pytest-invenio (>=1.4.0)"] -docs = ["Sphinx (>=3)"] -tests = ["pytest-cache (>=1.0)", "pytest-runner (>=2.6.2)", "pytest-invenio (>=1.4.0)"] +all = ["Sphinx (>=4.2.0)", "pytest-cache (>=1.0)", "pytest-invenio (>=1.4.0)", "pytest-runner (>=2.6.2)"] +docs = ["Sphinx (>=4.2.0)"] +tests = ["pytest-cache (>=1.0)", "pytest-invenio (>=1.4.0)", "pytest-runner (>=2.6.2)"] [[package]] name = "imagesize" -version = "1.3.0" +version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "main" optional = true @@ -378,20 +350,20 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.8.2" +version = "4.12.0" description = "Read metadata from Python packages" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "ipdb" @@ -408,7 +380,7 @@ toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} [[package]] name = "ipython" -version = "7.30.1" +version = "7.34.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false @@ -433,25 +405,19 @@ doc = ["Sphinx (>=1.3)"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] -notebook = ["notebook", "ipywidgets"] +notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] +test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments", "requests", "testpath"] [[package]] -name = "isbnid-fork" -version = "0.5.2" -description = "Python ISBN ids" +name = "isbnlib" +version = "3.10.10" +description = "Extract, clean, transform, hyphenate and metadata for ISBNs (International Standard Book Number)." category = "main" optional = false python-versions = "*" -[package.dependencies] -autosemver = ">=0.2,<1.0" - -[package.extras] -tests = ["pytest-pep8 (>=1.0.6)", "pytest (>=3.0.4)"] - [[package]] name = "isodate" version = "0.6.1" @@ -472,10 +438,10 @@ optional = false python-versions = ">=3.6.1,<4.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] +requirements_deprecated_finder = ["pip-api", "pipreqs"] [[package]] name = "jedi" @@ -494,11 +460,11 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" -version = "3.0.3" +version = "3.1.2" description = "A very fast and expressive template engine." category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=2.0" @@ -522,7 +488,7 @@ six = ">=1.11.0" [package.extras] format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] -format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] +format_nongpl = ["idna", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "webcolors"] [[package]] name = "livereload" @@ -538,7 +504,7 @@ tornado = {version = "*", markers = "python_version > \"2.7\""} [[package]] name = "lxml" -version = "4.7.1" +version = "4.9.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -552,11 +518,11 @@ source = ["Cython (>=0.29.7)"] [[package]] name = "markupsafe" -version = "2.0.1" +version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "matplotlib-inline" @@ -629,7 +595,7 @@ python-versions = "*" [[package]] name = "pika" -version = "1.2.0" +version = "1.3.0" description = "Pika Python AMQP Client Library" category = "main" optional = false @@ -642,7 +608,7 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.24" +version = "3.0.30" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false @@ -653,7 +619,7 @@ wcwidth = "*" [[package]] name = "psycopg2-binary" -version = "2.9.2" +version = "2.9.3" description = "psycopg2 - Python-PostgreSQL Database Adapter" category = "main" optional = false @@ -669,15 +635,15 @@ python-versions = "*" [[package]] name = "pygments" -version = "2.10.0" +version = "2.12.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [[package]] name = "pyjwt" -version = "2.3.0" +version = "2.4.0" description = "JSON Web Token implementation in Python" category = "dev" optional = false @@ -685,9 +651,9 @@ python-versions = ">=3.6" [package.extras] crypto = ["cryptography (>=3.3.1)"] -dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.3.1)", "mypy", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pyoai" @@ -710,22 +676,22 @@ resolved_reference = "5f6eba1201270d930ed684e15e9b9fc885649d17" [[package]] name = "pyparsing" -version = "3.0.6" -description = "Python parsing module" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyrsistent" -version = "0.18.0" +version = "0.18.1" description = "Persistent/Functional/Immutable data structures" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "python-box" @@ -736,9 +702,9 @@ optional = false python-versions = ">=3.6" [package.extras] -pyyaml = ["pyyaml"] -all = ["ruamel.yaml", "toml", "msgpack"] +all = ["msgpack", "ruamel.yaml", "toml"] msgpack = ["msgpack"] +pyyaml = ["pyyaml"] "ruamel.yaml" = ["ruamel.yaml"] toml = ["toml"] yaml = ["ruamel.yaml"] @@ -764,7 +730,7 @@ python-versions = "*" [[package]] name = "pytz" -version = "2021.3" +version = "2022.1" description = "World timezone definitions, modern and historical" category = "main" optional = false @@ -772,11 +738,11 @@ python-versions = "*" [[package]] name = "pyyaml" -version = "5.4.1" +version = "6.0" description = "YAML parser and emitter for Python" category = "main" optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.6" [[package]] name = "rdflib" @@ -795,7 +761,7 @@ six = "*" docs = ["sphinx (<3)", "sphinxcontrib-apidoc"] html = ["html5lib"] sparql = ["requests"] -tests = ["html5lib", "networkx", "nose", "doctest-ignore-unicode"] +tests = ["doctest-ignore-unicode", "html5lib", "networkx", "nose"] [[package]] name = "redis" @@ -810,29 +776,29 @@ hiredis = ["hiredis (>=0.1.3)"] [[package]] name = "regex" -version = "2021.11.10" +version = "2022.7.9" description = "Alternative regular expression module, to replace re." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "requests" -version = "2.26.0" +version = "2.28.1" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "responses" @@ -848,7 +814,7 @@ six = "*" urllib3 = ">=1.25.10" [package.extras] -tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "types-mock", "types-requests", "types-six", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] +tests = ["coverage (>=3.7.1,<6.0.0)", "flake8", "mypy", "pytest (>=4.6)", "pytest (>=4.6,<5.0)", "pytest-cov", "pytest-localserver", "types-mock", "types-requests", "types-six"] [[package]] name = "six" @@ -868,7 +834,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "4.3.1" +version = "4.5.0" description = "Python documentation generator" category = "main" optional = true @@ -880,6 +846,7 @@ babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} docutils = ">=0.14,<0.18" imagesize = "*" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} Jinja2 = ">=2.3" packaging = "*" Pygments = ">=2.0" @@ -894,8 +861,8 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.900)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] -test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "types-requests", "types-typed-ast"] +test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast"] [[package]] name = "sphinx-autobuild" @@ -926,7 +893,7 @@ docutils = "<0.17" sphinx = "*" [package.extras] -dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] [[package]] name = "sphinxcontrib-applehelp" @@ -937,7 +904,7 @@ optional = true python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] @@ -949,7 +916,7 @@ optional = true python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] @@ -961,8 +928,8 @@ optional = true python-versions = ">=3.6" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest", "html5lib"] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jsmath" @@ -973,7 +940,7 @@ optional = true python-versions = ">=3.5" [package.extras] -test = ["pytest", "flake8", "mypy"] +test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" @@ -984,7 +951,7 @@ optional = true python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] @@ -996,7 +963,7 @@ optional = true python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] @@ -1009,7 +976,7 @@ python-versions = ">=3.5" [[package]] name = "structlog" -version = "21.4.0" +version = "21.5.0" description = "Structured Logging for Python" category = "main" optional = false @@ -1019,9 +986,9 @@ python-versions = ">=3.6" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -dev = ["pre-commit", "rich", "cogapp", "tomli", "coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest (>=6.0)", "simplejson", "furo", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] +dev = ["cogapp", "coverage", "freezegun (>=0.2.8)", "furo", "pre-commit", "pretend", "pytest (>=6.0)", "pytest-asyncio", "rich", "simplejson", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "tomli", "twisted"] docs = ["furo", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] -tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest-asyncio", "pytest (>=6.0)", "simplejson"] +tests = ["coverage", "freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio", "simplejson"] [[package]] name = "tblib" @@ -1041,26 +1008,26 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tornado" -version = "6.1" +version = "6.2" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." category = "main" optional = true -python-versions = ">= 3.5" +python-versions = ">= 3.7" [[package]] name = "traitlets" -version = "5.1.1" -description = "Traitlets Python configuration system" +version = "5.3.0" +description = "" category = "dev" optional = false python-versions = ">=3.7" [package.extras] -test = ["pytest"] +test = ["pre-commit", "pytest"] [[package]] name = "typed-ast" -version = "1.5.1" +version = "1.5.4" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false @@ -1068,23 +1035,23 @@ python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.7" +version = "1.26.10" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -1105,15 +1072,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "zipp" -version = "3.6.0" +version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] docs = ["Sphinx", "sphinx-autobuild", "sphinx-rtd-theme"] @@ -1123,7 +1090,7 @@ swagger = ["PyYAML"] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "c0f6e79d018c633ee4c3b935d5253e27d9e155e0f2b5a33f0e9d0a67b62227a3" +content-hash = "c77ed2fc3edfd0948e6f7775ffd428a5a3a5bf4f8f952671afcaad771fcc9c79" [metadata.files] alabaster = [ @@ -1135,27 +1102,24 @@ appdirs = [ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] appnope = [ - {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, - {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, ] asgiref = [ - {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, - {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, + {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, + {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, ] asttokens = [ {file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"}, {file = "asttokens-2.0.5.tar.gz", hash = "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5"}, ] attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, -] -autosemver = [ - {file = "autosemver-0.5.5.tar.gz", hash = "sha256:0af1e8a9c3604545c067311f1c26403e8f0d60b5d9561c0217e14eee21c98b02"}, + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] babel = [ - {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, - {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, + {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, + {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, @@ -1165,20 +1129,20 @@ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] certifi = [ - {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, - {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, - {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, + {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, + {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, ] click = [ - {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, - {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] coverage = [ {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, @@ -1239,16 +1203,16 @@ datacite = [ {file = "datacite-1.1.2.tar.gz", hash = "sha256:0164bc2ff35bba643897201eb359611abb43ff5811a9ac17fb5592cd643b4443"}, ] decorator = [ - {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, - {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] django = [ - {file = "Django-3.2.10-py3-none-any.whl", hash = "sha256:df6f5eb3c797b27c096d61494507b7634526d4ce8d7c8ca1e57a4fb19c0738a3"}, - {file = "Django-3.2.10.tar.gz", hash = "sha256:074e8818b4b40acdc2369e67dcd6555d558329785408dcd25340ee98f1f1d5c4"}, + {file = "Django-3.2.14-py3-none-any.whl", hash = "sha256:a8681e098fa60f7c33a4b628d6fcd3fe983a0939ff1301ecacac21d0b38bad56"}, + {file = "Django-3.2.14.tar.gz", hash = "sha256:677182ba8b5b285a4e072f3ac17ceee6aff1b5ce77fd173cc5b6a2d3dc022fcf"}, ] django-debug-toolbar = [ - {file = "django-debug-toolbar-3.2.3.tar.gz", hash = "sha256:95880677ea846ba1077d02305fd5e2b25e1da096e1d4a735b665e3340fa2ae79"}, - {file = "django_debug_toolbar-3.2.3-py3-none-any.whl", hash = "sha256:516702e1d71302bbc06059fa3c41efd1a3bd9cbb5580fc793343118d95b309e0"}, + {file = "django-debug-toolbar-3.5.0.tar.gz", hash = "sha256:97965f2630692de316ea0c1ca5bfa81660d7ba13146dbc6be2059cf55b35d0e5"}, + {file = "django_debug_toolbar-3.5.0-py3-none-any.whl", hash = "sha256:89a52128309eb4da12738801ff0c202d2ff8730d1c3225fac6acf630c303e661"}, ] django-environ = [ {file = "django-environ-0.4.5.tar.gz", hash = "sha256:6c9d87660142608f63ec7d5ce5564c49b603ea8ff25da595fd6098f6dc82afde"}, @@ -1259,67 +1223,59 @@ django-split-settings = [ {file = "django_split_settings-1.1.0-py3-none-any.whl", hash = "sha256:5d97ae64cf9ed14a831722d82ac725944667ac8c08307b7cfd22e91367b411d0"}, ] django-watchman = [ - {file = "django-watchman-1.2.0.tar.gz", hash = "sha256:c38830c58984b8eb29db30a3e332968d1c7e235dee3f5c0a907d8d79a37a3125"}, - {file = "django_watchman-1.2.0-py2.py3-none-any.whl", hash = "sha256:6c0b8889456ed644fcfe2f7c3294cb4ef8568a85772b7f95842e2d8c9b4bbff5"}, + {file = "django-watchman-1.3.0.tar.gz", hash = "sha256:33b5fc734d689b83cb96fc17beda624ae2955f4cede0856897d990c363eac962"}, + {file = "django_watchman-1.3.0-py2.py3-none-any.whl", hash = "sha256:5f04300bd7fbdd63b8a883b2730ed1e4d9b0f9991133b33a1281134b81f466eb"}, ] djangorestframework = [ - {file = "djangorestframework-3.13.0-py3-none-any.whl", hash = "sha256:48e64f08244fa0df9e2b8fbd405edec263d8e1251112a06d0073b546b7c86b9c"}, - {file = "djangorestframework-3.13.0.tar.gz", hash = "sha256:8b987d5683f5b3553dd946d4972048d3117fc526cb0bc01a3f021e81af53f39e"}, + {file = "djangorestframework-3.13.1-py3-none-any.whl", hash = "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"}, + {file = "djangorestframework-3.13.1.tar.gz", hash = "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee"}, ] docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] -dulwich = [ - {file = "dulwich-0.19.16-cp27-cp27m-win32.whl", hash = "sha256:267160904e9a1cb6c248c5efc53597a35d038ecc6f60bdc4546b3053bed11982"}, - {file = "dulwich-0.19.16-cp27-cp27m-win_amd64.whl", hash = "sha256:4e3aba5e4844e7c700721c1fc696987ea820ee3528a03604dc4e74eff4196826"}, - {file = "dulwich-0.19.16-cp35-cp35m-win_amd64.whl", hash = "sha256:60bb2c2c92f5025c1b53a556304008f0f624c98ae36f22d870e056b2d4236c11"}, - {file = "dulwich-0.19.16-cp36-cp36m-win_amd64.whl", hash = "sha256:f00d132082b8fcc2eb0d722abc773d4aeb5558c1475d7edd1f0f571146c29db9"}, - {file = "dulwich-0.19.16-py2-none-any.whl", hash = "sha256:10699277c6268d0c16febe141a5b1c1a6e9744f3144c2d2de1706f4b1adafe63"}, - {file = "dulwich-0.19.16-py3-none-any.whl", hash = "sha256:dddae02d372fc3b5cfb0046d0f62246ef281fa0c088df7601ab5916607add94b"}, - {file = "dulwich-0.19.16.tar.gz", hash = "sha256:f74561c448bfb6f04c07de731c1181ae4280017f759b0bb04fa5770aa84ca850"}, -] elasticsearch = [ - {file = "elasticsearch-7.16.1-py2.py3-none-any.whl", hash = "sha256:97e9d62db71a571c540d6dfa2a51bc2263b5c0d7b9f9896eb7116ba02373c69b"}, - {file = "elasticsearch-7.16.1.tar.gz", hash = "sha256:c024ee2e7e2509c842c4e3c5e2b99a92ceecfde06d6dac2d32a19bf566c3e175"}, + {file = "elasticsearch-7.17.4-py2.py3-none-any.whl", hash = "sha256:81c1d15b0f9382b2dc40a896c634f8de09bfcd5079e19a8412940a9ddfbde64b"}, + {file = "elasticsearch-7.17.4.tar.gz", hash = "sha256:540d646908761120926850e98230ffc08fee9a3f9b45073d42de08246f598652"}, ] executing = [ - {file = "executing-0.8.2-py2.py3-none-any.whl", hash = "sha256:32fc6077b103bd19e6494a72682d66d5763cf20a106d5aa7c5ccbea4e47b0df7"}, - {file = "executing-0.8.2.tar.gz", hash = "sha256:c23bf42e9a7b9b212f185b1b2c3c91feb895963378887bb10e64a2e612ec0023"}, + {file = "executing-0.8.3-py2.py3-none-any.whl", hash = "sha256:d1eef132db1b83649a3905ca6dd8897f71ac6f8cac79a7e58a1a09cf137546c9"}, + {file = "executing-0.8.3.tar.gz", hash = "sha256:c6554e21c6b060590a6d3be4b82fb78f8f0194d809de5ea7df1c093763311501"}, ] gunicorn = [ {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] icecream = [ - {file = "icecream-2.1.1-py2.py3-none-any.whl", hash = "sha256:adc1c48f5a4f83b2171c774a35142f217d317a84fca8cdf3fc65aa1321ff26b6"}, - {file = "icecream-2.1.1.tar.gz", hash = "sha256:47e00e3f4e8477996e7dc420b6fa8ba53f8ced17de65320fedb5b15997b76589"}, + {file = "icecream-2.1.3-py2.py3-none-any.whl", hash = "sha256:757aec31ad4488b949bc4f499d18e6e5973c40cc4d4fc607229e78cfaec94c34"}, + {file = "icecream-2.1.3.tar.gz", hash = "sha256:0aa4a7c3374ec36153a1d08f81e3080e83d8ac1eefd97d2f4fe9544e8f9b49de"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] idutils = [ - {file = "IDUtils-1.1.9-py2.py3-none-any.whl", hash = "sha256:01edcde8394b73dc725958770e6e8d20088790345639982698715749f8213acc"}, - {file = "IDUtils-1.1.9.tar.gz", hash = "sha256:80c10f2ecb5cf020aa788ea860fa1f450a5a6714a0e95ae62334d5d286b395be"}, + {file = "IDUtils-1.1.12-py2.py3-none-any.whl", hash = "sha256:c625193d7991f5c396aaf4c6af83865fb032ce51e1f60e7271d129d8df2bd2b1"}, + {file = "IDUtils-1.1.12.tar.gz", hash = "sha256:6246205d35d07466c9ce807a2bdf616eaacb3b8fc2934ccdf1bf51ea41785de7"}, ] imagesize = [ - {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, - {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, - {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, + {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, + {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, ] ipdb = [ {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, ] ipython = [ - {file = "ipython-7.30.1-py3-none-any.whl", hash = "sha256:fc60ef843e0863dd4e24ab2bb5698f071031332801ecf8d1aeb4fb622056545c"}, - {file = "ipython-7.30.1.tar.gz", hash = "sha256:cb6aef731bf708a7727ab6cde8df87f0281b1427d41e65d62d4b68934fa54e97"}, + {file = "ipython-7.34.0-py3-none-any.whl", hash = "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e"}, + {file = "ipython-7.34.0.tar.gz", hash = "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6"}, ] -isbnid-fork = [ - {file = "isbnid_fork-0.5.2.tar.gz", hash = "sha256:8d878866aa0e7f06e700a37fce586c7398ce4837da8bca39683db7028a9c3837"}, +isbnlib = [ + {file = "isbnlib-3.10.10-py2.py3-none-any.whl", hash = "sha256:623a09329e8ec7049edf15dd412db042bf4f8236a428bf7a22d84a125584f52d"}, + {file = "isbnlib-3.10.10.tar.gz", hash = "sha256:c9e6c1dcaa9dff195429373cf2beb3117f30b3fca43d7db5aec5a2d1f6f59784"}, ] isodate = [ {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, @@ -1334,8 +1290,8 @@ jedi = [ {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, ] jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] jsonschema = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, @@ -1345,137 +1301,118 @@ livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] lxml = [ - {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, - {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, - {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"}, - {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"}, - {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"}, - {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"}, - {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"}, - {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"}, - {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"}, - {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"}, - {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"}, - {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"}, - {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"}, - {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"}, - {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"}, - {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"}, - {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"}, - {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"}, - {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"}, - {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"}, - {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"}, - {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"}, - {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"}, - {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"}, - {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"}, - {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"}, - {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"}, - {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"}, - {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"}, - {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"}, - {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, - {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, + {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"}, + {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc"}, + {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc"}, + {file = "lxml-4.9.1-cp27-cp27m-win32.whl", hash = "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3"}, + {file = "lxml-4.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627"}, + {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84"}, + {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8"}, + {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8"}, + {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d"}, + {file = "lxml-4.9.1-cp310-cp310-win32.whl", hash = "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7"}, + {file = "lxml-4.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b"}, + {file = "lxml-4.9.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d"}, + {file = "lxml-4.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3"}, + {file = "lxml-4.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29"}, + {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d"}, + {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318"}, + {file = "lxml-4.9.1-cp35-cp35m-win32.whl", hash = "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7"}, + {file = "lxml-4.9.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4"}, + {file = "lxml-4.9.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf"}, + {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3"}, + {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391"}, + {file = "lxml-4.9.1-cp36-cp36m-win32.whl", hash = "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e"}, + {file = "lxml-4.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7"}, + {file = "lxml-4.9.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca"}, + {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785"}, + {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785"}, + {file = "lxml-4.9.1-cp37-cp37m-win32.whl", hash = "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a"}, + {file = "lxml-4.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e"}, + {file = "lxml-4.9.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715"}, + {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036"}, + {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387"}, + {file = "lxml-4.9.1-cp38-cp38-win32.whl", hash = "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94"}, + {file = "lxml-4.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345"}, + {file = "lxml-4.9.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000"}, + {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25"}, + {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd"}, + {file = "lxml-4.9.1-cp39-cp39-win32.whl", hash = "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb"}, + {file = "lxml-4.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73"}, + {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c"}, + {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9"}, + {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] matplotlib-inline = [ {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"}, @@ -1506,90 +1443,110 @@ pickleshare = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] pika = [ - {file = "pika-1.2.0-py2.py3-none-any.whl", hash = "sha256:59da6701da1aeaf7e5e93bb521cc03129867f6e54b7dd352c4b3ecb2bd7ec624"}, - {file = "pika-1.2.0.tar.gz", hash = "sha256:f023d6ac581086b124190cb3dc81dd581a149d216fa4540ac34f9be1e3970b89"}, + {file = "pika-1.3.0-py3-none-any.whl", hash = "sha256:9195f37aed089862b205fd8f8ce1cc6ea0a7ee3cd80f58e6eea6cb9d8411a647"}, + {file = "pika-1.3.0.tar.gz", hash = "sha256:15357ddc47a5c28f0b07d80e93d504cbbf7a1ad5e1cd129ecd27afe76472c529"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.24-py3-none-any.whl", hash = "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506"}, - {file = "prompt_toolkit-3.0.24.tar.gz", hash = "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6"}, + {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"}, + {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"}, ] psycopg2-binary = [ - {file = "psycopg2-binary-2.9.2.tar.gz", hash = "sha256:234b1f48488b2f86aac04fb00cb04e5e9bcb960f34fa8a8e41b73149d581a93b"}, - {file = "psycopg2_binary-2.9.2-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c0e1fb7097ded2cc44d9037cfc68ad86a30341261492e7de95d180e534969fb2"}, - {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:717525cdc97b23182ff6f470fb5bf6f0bc796b5a7000c6f6699d6679991e4a5e"}, - {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3865d0cd919349c45603bd7e80249a382c5ecf8106304cfd153282adf9684b6a"}, - {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:daf6b5c62eb738872d61a1fa740d7768904911ba5a7e055ed72169d379b58beb"}, - {file = "psycopg2_binary-2.9.2-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:3ac83656ff4fbe7f2a956ab085e3eb1d678df54759965d509bdd6a06ce520d49"}, - {file = "psycopg2_binary-2.9.2-cp310-cp310-win32.whl", hash = "sha256:a04cfa231e7d9b63639e62166a4051cb47ca599fa341463fa3e1c48585fcee64"}, - {file = "psycopg2_binary-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:c6e16e085fe6dc6c099ee0be56657aa9ad71027465ef9591d302ba230c404c7e"}, - {file = "psycopg2_binary-2.9.2-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:53912199abb626a7249c662e72b70b4f57bf37f840599cec68625171435790dd"}, - {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:029e09a892b9ebc3c77851f69ce0720e1b72a9c6850460cee49b14dfbf9ccdd2"}, - {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db1b03c189f85b8df29030ad32d521dd7dcb862fd5f8892035314f5b886e70ce"}, - {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:2eecbdc5fa5886f2dd6cc673ce4291cc0fb8900965315268960ad9c2477f8276"}, - {file = "psycopg2_binary-2.9.2-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:a77e98c68b0e6c51d4d6a994d22b30e77276cbd33e4aabdde03b9ad3a2c148aa"}, - {file = "psycopg2_binary-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:bf31e6fdb4ec1f6d98a07f48836508ed6edd19b48b13bbf168fbc1bd014b4ca2"}, - {file = "psycopg2_binary-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f9c37ecb173d76cf49e519133fd70851b8f9c38b6b8c1cb7fcfc71368d4cc6fc"}, - {file = "psycopg2_binary-2.9.2-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:a507db7758953b1b170c4310691a1a89877029b1e11b08ba5fc8ae3ddb35596b"}, - {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e4bbcfb403221ea1953f3e0a85cef00ed15c1683a66cf35c956a7e37c33a4c4"}, - {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4dff0f15af6936c6fe6da7067b4216edbbe076ad8625da819cc066591b1133c"}, - {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:8d2aafe46eb87742425ece38130510fbb035787ee89a329af299029c4d9ae318"}, - {file = "psycopg2_binary-2.9.2-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:37c8f00f7a2860bac9f7a54f03c243fc1dd9b367e5b2b52f5a02e5f4e9d8c49b"}, - {file = "psycopg2_binary-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:ef97578fab5115e3af4334dd3376dea3c3a79328a3314b21ec7ced02920b916d"}, - {file = "psycopg2_binary-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7e6bd4f532c2cd297b81114526176b240109a1c52020adca69c3f3226c65dc18"}, - {file = "psycopg2_binary-2.9.2-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:eeee7b18c51d02e49bf1984d7af26e8843fe68e31fa1cbab5366ebdfa1c89ade"}, - {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:497372cc76e6cbce2f51b37be141f360a321423c03eb9be45524b1d123f4cd11"}, - {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671699aff57d22a245b7f4bba89e3de97dc841c5e98bd7f685429b2b20eca47"}, - {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:b9d45374ba98c1184df9cce93a0b766097544f8bdfcd5de83ff10f939c193125"}, - {file = "psycopg2_binary-2.9.2-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:a1852c5bef7e5f52bd43fde5eda610d4df0fb2efc31028150933e84b4140d47a"}, - {file = "psycopg2_binary-2.9.2-cp38-cp38-win32.whl", hash = "sha256:108b0380969ddab7c8ef2a813a57f87b308b2f88ec15f1a1e7b653964a3cfb25"}, - {file = "psycopg2_binary-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:14427437117f38e65f71db65d8eafd0e86837be456567798712b8da89db2b2dd"}, - {file = "psycopg2_binary-2.9.2-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:578c279cd1ce04f05ae0912530ece00bab92854911808e5aec27588aba87e361"}, - {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2dea4deac3dd3687e32daeb0712ee96c535970dfdded37a11de6a21145ab0e"}, - {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b592f09ff18cfcc9037b9a976fcd62db48cae9dbd5385f2471d4c2ba40c52b4d"}, - {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:3a320e7a804f3886a599fea507364aaafbb8387027fffcdfbd34d96316c806c7"}, - {file = "psycopg2_binary-2.9.2-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7585ca73dcfe326f31fafa8f96e6bb98ea9e9e46c7a1924ec8101d797914ae27"}, - {file = "psycopg2_binary-2.9.2-cp39-cp39-win32.whl", hash = "sha256:9c0aaad07941419926b9bd00171e49fe6b06e42e5527fb91671e137fe6c93d77"}, - {file = "psycopg2_binary-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa2847d8073951dbc84c4f8b32c620764db3c2eb0d99a04835fecfab7d04816e"}, + {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"}, ] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] pygments = [ - {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, - {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, + {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, + {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, ] pyjwt = [ - {file = "PyJWT-2.3.0-py3-none-any.whl", hash = "sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"}, - {file = "PyJWT-2.3.0.tar.gz", hash = "sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41"}, + {file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"}, + {file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"}, ] pyoai = [] pyparsing = [ - {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, - {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pyrsistent = [ - {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2"}, - {file = "pyrsistent-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427"}, - {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef"}, - {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c"}, - {file = "pyrsistent-0.18.0-cp38-cp38-win32.whl", hash = "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78"}, - {file = "pyrsistent-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b"}, - {file = "pyrsistent-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4"}, - {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680"}, - {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426"}, - {file = "pyrsistent-0.18.0-cp39-cp39-win32.whl", hash = "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b"}, - {file = "pyrsistent-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea"}, - {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, + {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, + {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26"}, + {file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e"}, + {file = "pyrsistent-0.18.1-cp310-cp310-win32.whl", hash = "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6"}, + {file = "pyrsistent-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec"}, + {file = "pyrsistent-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b"}, + {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc"}, + {file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22"}, + {file = "pyrsistent-0.18.1-cp37-cp37m-win32.whl", hash = "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8"}, + {file = "pyrsistent-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286"}, + {file = "pyrsistent-0.18.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"}, + {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec"}, + {file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c"}, + {file = "pyrsistent-0.18.1-cp38-cp38-win32.whl", hash = "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca"}, + {file = "pyrsistent-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a"}, + {file = "pyrsistent-0.18.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5"}, + {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045"}, + {file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c"}, + {file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"}, + {file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"}, + {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, ] python-box = [ {file = "python-box-5.4.1.tar.gz", hash = "sha256:b68e0f8abc86f3deda751b3390f64df64a0989459de51ba4db949662a7b4d8ac"}, @@ -1607,39 +1564,43 @@ python-simplexquery = [ {file = "python-simplexquery-1.0.5.3.zip", hash = "sha256:4849070678538d26778c9902c58eac13a88beaffc526e69ee5e3db3744499a2b"}, ] pytz = [ - {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, - {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, + {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, + {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, ] pyyaml = [ - {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, - {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, - {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, - {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, - {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, - {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, - {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, - {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, - {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] rdflib = [ {file = "rdflib-5.0.0-py3-none-any.whl", hash = "sha256:88208ea971a87886d60ae2b1a4b2cdc263527af0454c422118d43fe64b357877"}, @@ -1650,84 +1611,84 @@ redis = [ {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] regex = [ - {file = "regex-2021.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf"}, - {file = "regex-2021.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9ed0b1e5e0759d6b7f8e2f143894b2a7f3edd313f38cf44e1e15d360e11749b"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:473e67837f786404570eae33c3b64a4b9635ae9f00145250851a1292f484c063"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2fee3ed82a011184807d2127f1733b4f6b2ff6ec7151d83ef3477f3b96a13d03"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d5fd67df77bab0d3f4ea1d7afca9ef15c2ee35dfb348c7b57ffb9782a6e4db6e"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5d408a642a5484b9b4d11dea15a489ea0928c7e410c7525cd892f4d04f2f617b"}, - {file = "regex-2021.11.10-cp310-cp310-win32.whl", hash = "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a"}, - {file = "regex-2021.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12"}, - {file = "regex-2021.11.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:74cbeac0451f27d4f50e6e8a8f3a52ca074b5e2da9f7b505c4201a57a8ed6286"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:3598893bde43091ee5ca0a6ad20f08a0435e93a69255eeb5f81b85e81e329264"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:50a7ddf3d131dc5633dccdb51417e2d1910d25cbcf842115a3a5893509140a3a"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:61600a7ca4bcf78a96a68a27c2ae9389763b5b94b63943d5158f2a377e09d29a"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:563d5f9354e15e048465061509403f68424fef37d5add3064038c2511c8f5e00"}, - {file = "regex-2021.11.10-cp36-cp36m-win32.whl", hash = "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4"}, - {file = "regex-2021.11.10-cp36-cp36m-win_amd64.whl", hash = "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e"}, - {file = "regex-2021.11.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:42b50fa6666b0d50c30a990527127334d6b96dd969011e843e726a64011485da"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6e1d2cc79e8dae442b3fa4a26c5794428b98f81389af90623ffcc650ce9f6732"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:0416f7399e918c4b0e074a0f66e5191077ee2ca32a0f99d4c187a62beb47aa05"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ce298e3d0c65bd03fa65ffcc6db0e2b578e8f626d468db64fdf8457731052942"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dc07f021ee80510f3cd3af2cad5b6a3b3a10b057521d9e6aaeb621730d320c5a"}, - {file = "regex-2021.11.10-cp37-cp37m-win32.whl", hash = "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec"}, - {file = "regex-2021.11.10-cp37-cp37m-win_amd64.whl", hash = "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4"}, - {file = "regex-2021.11.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83"}, - {file = "regex-2021.11.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f5be7805e53dafe94d295399cfbe5227f39995a997f4fd8539bf3cbdc8f47ca8"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a955b747d620a50408b7fdf948e04359d6e762ff8a85f5775d907ceced715129"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:139a23d1f5d30db2cc6c7fd9c6d6497872a672db22c4ae1910be22d4f4b2068a"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ca49e1ab99593438b204e00f3970e7a5f70d045267051dfa6b5f4304fcfa1dbf"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:96fc32c16ea6d60d3ca7f63397bff5c75c5a562f7db6dec7d412f7c4d2e78ec0"}, - {file = "regex-2021.11.10-cp38-cp38-win32.whl", hash = "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc"}, - {file = "regex-2021.11.10-cp38-cp38-win_amd64.whl", hash = "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d"}, - {file = "regex-2021.11.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b"}, - {file = "regex-2021.11.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cd410a1cbb2d297c67d8521759ab2ee3f1d66206d2e4328502a487589a2cb21b"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e6096b0688e6e14af6a1b10eaad86b4ff17935c49aa774eac7c95a57a4e8c296"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:529801a0d58809b60b3531ee804d3e3be4b412c94b5d267daa3de7fadef00f49"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f594b96fe2e0821d026365f72ac7b4f0b487487fb3d4aaf10dd9d97d88a9737"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2409b5c9cef7054dde93a9803156b411b677affc84fca69e908b1cb2c540025d"}, - {file = "regex-2021.11.10-cp39-cp39-win32.whl", hash = "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a"}, - {file = "regex-2021.11.10-cp39-cp39-win_amd64.whl", hash = "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29"}, - {file = "regex-2021.11.10.tar.gz", hash = "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6"}, + {file = "regex-2022.7.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d35bbcbf70d14f724e7489746cf68efe122796578addd98f91428e144d0ad266"}, + {file = "regex-2022.7.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12e1404dfb4e928d3273a10e3468877fe84bdcd3c50b655a2c9613cfc5d9fe63"}, + {file = "regex-2022.7.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727edff0a4eaff3b6d26cbb50216feac9055aba7e6290eec23c061c2fe2fab55"}, + {file = "regex-2022.7.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18e6203cfd81df42a987175aaeed7ba46bcb42130cd81763e2d5edcff0006d5d"}, + {file = "regex-2022.7.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:06c509bd7dcb7966bdb03974457d548e54d8327bad5b0c917e87248edc43e2eb"}, + {file = "regex-2022.7.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f94b0befc811fe74a972b1739fffbf74c0dc1a91102aca8e324aa4f2c6991bd"}, + {file = "regex-2022.7.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:121981ba84309dabefd5e1debd49be6d51624e54b4d44bfc184cd8d555ff1df1"}, + {file = "regex-2022.7.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d40b4447784dbe0896a6d10a178f6724598161f942c56f5a60dc0ef7fe63f7a1"}, + {file = "regex-2022.7.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:414ae507ba88264444baf771fec43ce0adcd4c5dbb304d3e0716f3f4d4499d2e"}, + {file = "regex-2022.7.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9e4006942334fa954ebd32fa0728718ec870f95f4ba7cda9edc46dd49c294f22"}, + {file = "regex-2022.7.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c1821146b429e6fdbd13ea10f26765e48d5284bc79749468cfbfe3ceb929f0d"}, + {file = "regex-2022.7.9-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:402fa998c5988d11ed34585eb65740dcebd0fd11844d12eb0a6b4be178eb9c64"}, + {file = "regex-2022.7.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d2672d68cf6c8452b6758fc3cd2d8feac966d511eed79a68182a5297b473af9c"}, + {file = "regex-2022.7.9-cp310-cp310-win32.whl", hash = "sha256:2e5db20412f0db8798ff72473d16da5f13ec808e975b49188badb2462f529fa9"}, + {file = "regex-2022.7.9-cp310-cp310-win_amd64.whl", hash = "sha256:667a06bb8d72b6da3d9cf38dac4ba969688868ed2279a692e993d2c0e1c30aba"}, + {file = "regex-2022.7.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b72a4ec79a15f6066d14ae1c472b743af4b4ecee14420e8d6e4a336b49b8f21c"}, + {file = "regex-2022.7.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea27acd97a752cfefa9907da935e583efecb302e6e9866f37565968c8407ad58"}, + {file = "regex-2022.7.9-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42da079e31ae9818ffa7a35cdd16ab7104e3f7eca9c0958040aede827b2e55c6"}, + {file = "regex-2022.7.9-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab1cb36b411f16da6e057ef8e6657dd0af36f59a667f07e0b4b617e44e53d7b2"}, + {file = "regex-2022.7.9-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7f5ccfff648093152cadf6d886c7bd922047532f72024c953a79c7553aac2fe"}, + {file = "regex-2022.7.9-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9daeccb2764bf4cc280c40c6411ae176bb0876948e536590a052b3d647254c95"}, + {file = "regex-2022.7.9-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:673549a0136c7893f567ed71ab5225ed3701c79b17c0a7faee846c645fc24010"}, + {file = "regex-2022.7.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0fd8c3635fa03ef79d07c7b3ed693b3f3930ccb52c0c51761c3296a7525b135c"}, + {file = "regex-2022.7.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:a048f91823862270905cb22ef88038b08aac852ce48e0ecc4b4bf1b895ec37d9"}, + {file = "regex-2022.7.9-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:f8a2fd2f62a77536e4e3193303bec380df40d99e253b1c8f9b6eafa07eaeff67"}, + {file = "regex-2022.7.9-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ab0709daedc1099bbd4371ae17eeedd4efc1cf70fcdcfe5de1374a0944b61f80"}, + {file = "regex-2022.7.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ee769a438827e443ed428e66d0aa7131c653ecd86ddc5d4644a81ed1d93af0e7"}, + {file = "regex-2022.7.9-cp36-cp36m-win32.whl", hash = "sha256:e1fdda3ec7e9785065b67941693995cab95b54023a21db9bf39e54cc7b2c3526"}, + {file = "regex-2022.7.9-cp36-cp36m-win_amd64.whl", hash = "sha256:00d2e907d3c5e4f85197c8d2263a9cc2d34bf234a9c6236ae42a3fb0bc09b759"}, + {file = "regex-2022.7.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5b1cffff2d9f832288fe516021cb81c95c57c0067b13a82f1d2daabdbc2f4270"}, + {file = "regex-2022.7.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfeb71095c8d8380a5df5a38ff94d27a3f483717e509130a822b4d6400b7991"}, + {file = "regex-2022.7.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0186edcda692c38381db8ac257c2d023fd2e08818d45dc5bee4ed84212045f51"}, + {file = "regex-2022.7.9-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc74f0171eede67d79a79c06eca0fe5b7b280dbb8c27ad1fae4ced2ad66268f"}, + {file = "regex-2022.7.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbdf4fc6adf38fab1091c579ece3fe9f493bd0f1cfc3d2c76d2e52461ca4f8a9"}, + {file = "regex-2022.7.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a00cd58a30a1041c193777cb1bc090200b05ff4b073d5935738afd1023e63069"}, + {file = "regex-2022.7.9-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d561dcb0fb0ab858291837d51330696a45fd3ba6912a332a4ee130e5484b9e47"}, + {file = "regex-2022.7.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ae1c5b435d44aa91d48cc710f20c3485e0584a3ad3565d5ae031d61a35f674f4"}, + {file = "regex-2022.7.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1703490c5b850fa9cef1af00c58966756042e6ca22f4fb5bb857345cd535834f"}, + {file = "regex-2022.7.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:782627a1cb8fbb1c78d8e841f5b71c2c683086c038f975bebdac7cce7678a96f"}, + {file = "regex-2022.7.9-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b279b9bb401af41130fd2a09427105100bc8c624ed45da1c81c1c0d0aa639734"}, + {file = "regex-2022.7.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:13d74951c14708f00700bb29475129ecbc40e01b4029c62ee7bfe9d1f59f31ce"}, + {file = "regex-2022.7.9-cp37-cp37m-win32.whl", hash = "sha256:1244e9b9b4b81c9c34e8a84273ffaeebdc78abc98a5b02dcdd49845eb3c63bd7"}, + {file = "regex-2022.7.9-cp37-cp37m-win_amd64.whl", hash = "sha256:67bd3bdd27db7a6460384869dd4b9c54267d805b67d70b20495bb5767f8e051c"}, + {file = "regex-2022.7.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:34ae4f35db30caa4caf85c55069fcb7a05966a3a5ba6e9e1dab5477d84fbb08a"}, + {file = "regex-2022.7.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f355caec5bbce20421dc26e53787b10e32fd0df68db2b795435217210c08d69c"}, + {file = "regex-2022.7.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d93167b7d7731fa9ff9fdc1bae84ec9c7133b01a35f8cc04e926d48da6ce1f7"}, + {file = "regex-2022.7.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ab39aa445d00902c43a1e951871bedc7f18d095a21eccba153d594faac34aea"}, + {file = "regex-2022.7.9-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1948d3ceac5b2d55bc93159c1e0679a256a87a54c735be5cef4543a9e692dbb9"}, + {file = "regex-2022.7.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0220a7a16fd4bfc700661f920510defd31ef7830ce992d5cc51777aa8ccd724"}, + {file = "regex-2022.7.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c6df8be7d1dd35a0d9a200fbc29f888c4452c8882d284f87608046152e049e6"}, + {file = "regex-2022.7.9-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7c5f914b0eb5242c09f91058b80295525897e873b522575ab235b48db125597"}, + {file = "regex-2022.7.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2cd93725911c0159d597b90c96151070ef7e0e67604637e2f2abe06c34bf079"}, + {file = "regex-2022.7.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:119091c675e6ad19da8770f89aa1d52f4ad2a2018d631956f3e90c45882df880"}, + {file = "regex-2022.7.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4c5913cb9769038bd03e42318955c2f15a688384a6a0b807bcfc8271603d9277"}, + {file = "regex-2022.7.9-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a3c47c71fde0c5d584402e67546c81af9951540f1f622d821e9c20761556473a"}, + {file = "regex-2022.7.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:40a28759d345c0bb1f5b0ac74ac04f5d48136019522c95c0ec4b07786f67ce20"}, + {file = "regex-2022.7.9-cp38-cp38-win32.whl", hash = "sha256:8e2075ed4ea2e231e2e98b16cfa5dae87e9a6045a71104525e1efc29aa8faa8e"}, + {file = "regex-2022.7.9-cp38-cp38-win_amd64.whl", hash = "sha256:9f1c8fffd4def0b76c0947b8cb261b266e31041785dc2dc2db7569407a2f54fe"}, + {file = "regex-2022.7.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:473a7d21932ce7c314953b33c32e63df690181860edcdf14bba1278cdf71b07f"}, + {file = "regex-2022.7.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:192c2784833aea6fc7b004730bf1b91b8b8c6b998b30271aaf3bd8adfef20a96"}, + {file = "regex-2022.7.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dce6b2ad817e3eb107f8704782b091b0631dd3adf47f14bdc086165d05b528b0"}, + {file = "regex-2022.7.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2fc1e3928c1189c0382c547c17717c6d9f425fffe619ef94270fe4c6c8be0a6"}, + {file = "regex-2022.7.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f32e0d1c7e7b0b9c3cac76f3d278e7ee6b99c95672d2c1c6ea625033431837c0"}, + {file = "regex-2022.7.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f87e9108bb532f8a1fc6bf7e69b930a35c7b0267b8fef0a3ede0bcb4c5aaa531"}, + {file = "regex-2022.7.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:303676797c4c7978726e74eb8255d68f7125a3a29da71ff453448f2117290e9a"}, + {file = "regex-2022.7.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a6d9ea727fd1233ee746bf44dd37e7d4320b3ed8ff09e73d7638c969b28d280f"}, + {file = "regex-2022.7.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7d462ba84655abeddae4dfc517fe1afefb5430b3b5acb0a954de12a47aea7183"}, + {file = "regex-2022.7.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e2a262ec85c595fc8e1f3162cafc654d2219125c00ea3a190c173cea70d2cc7a"}, + {file = "regex-2022.7.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:162a5939a6fdf48658d3565eeff35acdd207e07367bf5caaff3d9ea7cb77d7a9"}, + {file = "regex-2022.7.9-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d07d849c9e2eca80adb85d3567302a47195a603ad7b1f0a07508e253c041f954"}, + {file = "regex-2022.7.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:206a327e628bc529d64b21ff79a5e2564f5aec7dc7abcd4b2e8a4b271ec10550"}, + {file = "regex-2022.7.9-cp39-cp39-win32.whl", hash = "sha256:49fcb45931a693b0e901972c5e077ea2cf30ec39da699645c43cb8b1542c6e14"}, + {file = "regex-2022.7.9-cp39-cp39-win_amd64.whl", hash = "sha256:0a3f3f45c5902eb4d90266002ccb035531ae9b9278f6d5e8028247c34d192099"}, + {file = "regex-2022.7.9.tar.gz", hash = "sha256:601c99ac775b6c89699a48976f3dbb000b47d3ca59362c8abc9582e6d0780d91"}, ] requests = [ - {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, - {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] responses = [ {file = "responses-0.13.4-py2.py3-none-any.whl", hash = "sha256:d8d0f655710c46fd3513b9202a7f0dcedd02ca0f8cf4976f27fa8ab5b81e656d"}, @@ -1742,8 +1703,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] sphinx = [ - {file = "Sphinx-4.3.1-py3-none-any.whl", hash = "sha256:048dac56039a5713f47a554589dc98a442b39226a2b9ed7f82797fcb2fe9253f"}, - {file = "Sphinx-4.3.1.tar.gz", hash = "sha256:32a5b3e9a1b176cc25ed048557d4d3d01af635e6b76c5bc7a43b0a34447fbd45"}, + {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"}, + {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"}, ] sphinx-autobuild = [ {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, @@ -1782,8 +1743,8 @@ sqlparse = [ {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, ] structlog = [ - {file = "structlog-21.4.0-py3-none-any.whl", hash = "sha256:6ed8fadb27cf8362be0e606f5e79ccdd3b1e879aac65f9dc0ac3033fd013a7be"}, - {file = "structlog-21.4.0.tar.gz", hash = "sha256:305a66201f9605a2e8a2595271a446f258175901c09c01e4c2c2a8ac5b68edf1"}, + {file = "structlog-21.5.0-py3-none-any.whl", hash = "sha256:fd7922e195262b337da85c2a91c84be94ccab1f8fd1957bd6986f6904e3761c8"}, + {file = "structlog-21.5.0.tar.gz", hash = "sha256:68c4c29c003714fe86834f347cb107452847ba52414390a7ee583472bde00fc9"}, ] tblib = [ {file = "tblib-1.7.0-py2.py3-none-any.whl", hash = "sha256:289fa7359e580950e7d9743eab36b0691f0310fce64dee7d9c31065b8f723e23"}, @@ -1794,80 +1755,55 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tornado = [ - {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, - {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, - {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, - {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, - {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, - {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, - {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, - {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, - {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, - {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, - {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, - {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, - {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, - {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, - {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, - {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, - {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, - {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, - {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, - {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, - {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, - {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, - {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, - {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, - {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, - {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, - {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, - {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, - {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, - {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, - {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, - {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, - {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, - {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, - {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, - {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, - {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, - {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, - {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, - {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, - {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, + {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"}, + {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"}, + {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"}, + {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"}, + {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"}, + {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"}, + {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"}, ] traitlets = [ - {file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"}, - {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"}, + {file = "traitlets-5.3.0-py3-none-any.whl", hash = "sha256:65fa18961659635933100db8ca120ef6220555286949774b9cfc106f941d1c7a"}, + {file = "traitlets-5.3.0.tar.gz", hash = "sha256:0bb9f1f9f017aa8ec187d8b1b2a7a6626a2a1d877116baba52a129bfa124f8e2"}, ] typed-ast = [ - {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"}, - {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"}, - {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"}, - {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"}, - {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"}, - {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"}, - {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"}, - {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"}, - {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"}, - {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"}, - {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"}, - {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"}, - {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"}, - {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"}, - {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"}, - {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"}, - {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"}, - {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"}, - {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, ] urllib3 = [ - {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, - {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, + {file = "urllib3-1.26.10-py2.py3-none-any.whl", hash = "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec"}, + {file = "urllib3-1.26.10.tar.gz", hash = "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, @@ -1878,6 +1814,6 @@ xmltodict = [ {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, ] zipp = [ - {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, - {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, + {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, + {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, ] diff --git a/pyproject.toml b/pyproject.toml index 7a0af20b..fc6891cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Your Name "] [tool.poetry.dependencies] python = "^3.7" -Django = "<3.3" +Django = "<4.0" datacite = "^1.0.1" djangorestframework = "^3.12.4" gunicorn = "^20.1.0" @@ -22,14 +22,14 @@ django-environ = "^0.4.5" django-split-settings = "^1.0.1" rdflib = "^5.0.0" python-dateutil = "^2.8.1" -pyoai = {git = "https://github.com/infrae/pyoai", rev = "5f6eba12"} python-box = "^5.3.0" +pyoai = {git = "https://github.com/infrae/pyoai", rev = "5f6eba12"} python-simplexquery = {version = "*", optional = true} # These are here because of: https://github.com/python-poetry/poetry/issues/1644 Sphinx = {version = "^4.0.2", optional = true} sphinx-autobuild = {version = "^2021.3.14", optional = true} sphinx-rtd-theme = {version = "^0.5.2", optional = true} -PyYAML = { version = "^5.4.1", optional = true} +PyYAML = { version = "^6.0", optional = true} [tool.poetry.dev-dependencies] responses = "^0.13.2"