From de07d578f0e123f7d904a0406ff914091c041928 Mon Sep 17 00:00:00 2001 From: Ken Lippold Date: Mon, 4 Dec 2023 13:01:43 -0700 Subject: [PATCH] Added initial API endpoints for HydroShare archival --- core/endpoints/thing/schemas.py | 8 ++ core/endpoints/thing/views.py | 97 ++++++++++++++++++- ..._thing_hydroshare_archive_link_and_more.py | 23 +++++ core/models.py | 1 + environment.yml | 2 + requirements.txt | 2 + 6 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 core/migrations/0004_thing_hydroshare_archive_link_and_more.py diff --git a/core/endpoints/thing/schemas.py b/core/endpoints/thing/schemas.py index e91afd9e..fa50f354 100644 --- a/core/endpoints/thing/schemas.py +++ b/core/endpoints/thing/schemas.py @@ -104,3 +104,11 @@ class ThingMetadataGetResponse(Schema): class Config: allow_population_by_field_name = True + + +class ThingArchiveBody(Schema): + resource_title: str = Field(..., alias='resourceTitle') + resource_abstract: str = Field(..., alias='resourceAbstract') + resource_keywords: List[str] = Field(None, alias='resourceKeywords') + public_resource: bool = Field(False, alias='publicResource') + datastreams: List[UUID] = Field(None, alias='datastreams') diff --git a/core/endpoints/thing/views.py b/core/endpoints/thing/views.py index 45ed990e..a786f0f8 100644 --- a/core/endpoints/thing/views.py +++ b/core/endpoints/thing/views.py @@ -1,9 +1,13 @@ +import os +import hsclient +import tempfile from ninja import Path from typing import List, Optional from uuid import UUID from datetime import datetime from django.db import transaction, IntegrityError from django.db.models import Q +from hsmodels.schemas.fields import PointCoverage from accounts.auth.jwt import JWTAuth from accounts.auth.basic import BasicAuth from accounts.auth.anonymous import anonymous_auth @@ -14,12 +18,13 @@ transfer_observed_property_ownership from core.endpoints.processinglevel.utils import query_processing_levels, build_processing_level_response, \ transfer_processing_level_ownership -from core.endpoints.datastream.utils import query_datastreams, build_datastream_response +from core.endpoints.datastream.utils import query_datastreams, build_datastream_response, generate_csv from core.endpoints.datastream.schemas import DatastreamGetResponse from core.endpoints.sensor.utils import query_sensors, build_sensor_response, transfer_sensor_ownership from .schemas import ThingGetResponse, ThingPostBody, ThingPatchBody, ThingOwnershipPatchBody, ThingPrivacyPatchBody, \ - ThingMetadataGetResponse, LocationFields, ThingFields + ThingMetadataGetResponse, LocationFields, ThingFields, ThingArchiveBody from .utils import query_things, get_thing_by_id, build_thing_response, check_thing_by_id +from hydroserver import settings router = DataManagementRouter(tags=['Things']) @@ -414,10 +419,92 @@ def get_datastreams(request, thing_id: UUID = Path(...)): '{thing_id}/archive', auth=[JWTAuth(), BasicAuth()], response={ - 201: None + 201: str, + 401: str, + 403: str, + 404: str } ) -def archive_thing(request, thing_id: UUID = Path(...)): +def archive_thing(request, data: ThingArchiveBody, thing_id: UUID = Path(...)): """""" - return None + authenticated_user = request.authenticated_user + + thing = get_thing_by_id( + user=authenticated_user, + thing_id=thing_id, + require_ownership=True, + raise_http_errors=True + ) + + if thing.hydroshare_archive_link is not None: + return 403, 'This site has already been archived to HydroShare.' + + if authenticated_user.hydroshare_token is None: + return 403, 'You have not linked a HydroShare account to your HydroServer account.' + + datastream_query, _ = query_datastreams( + user=request.authenticated_user, + thing_ids=[thing_id], + ) + + datastreams = datastream_query.all() + + if data.datastreams: + datastreams = [ + datastream for datastream in datastreams if datastream.id in datastreams + ] + + hydroshare_service = hsclient.HydroShare( + client_id=settings.AUTHLIB_OAUTH_CLIENTS['hydroshare']['client_id'], + token={ + 'access_token': authenticated_user.hydroshare_token['access_token'], + 'token_type': authenticated_user.hydroshare_token['token_type'], + 'scope': authenticated_user.hydroshare_token['scope'], + 'state': '', + 'expires_in': authenticated_user.hydroshare_token['expires_in'], + 'refresh_token': authenticated_user.hydroshare_token['refresh_token'] + } + ) + + archive_resource = hydroshare_service.create() + archive_resource.metadata.title = data.resource_title + archive_resource.metadata.abstract = data.resource_abstract + archive_resource.metadata.subjects = data.resource_keywords + archive_resource.set_sharing_status(data.public_resource) + archive_resource.metadata.spatial_coverage = PointCoverage( + name=thing.location.name, + north=thing.location.latitude, + east=thing.location.longitude, + projection='WGS 84 EPSG:4326', + type='point', + units='Decimal degrees' + ) + archive_resource.metadata.additional_metadata = { + 'Sampling Feature Type': thing.sampling_feature_type, + 'Sampling Feature Code': thing.sampling_feature_code, + 'Site Type': thing.site_type + } + + if thing.data_disclaimer: + archive_resource.metadata.additional_metadata['Data Disclaimer'] = thing.data_disclaimer + + archive_resource.save() + + datastream_file_names = [] + + with tempfile.TemporaryDirectory() as temp_dir: + for datastream in datastreams: + temp_file_name = f'{datastream.description}.csv' + temp_file_index = 2 + while temp_file_name in datastream_file_names: + temp_file_name = f'{datastream.description} - {str(temp_file_index)}.csv' + temp_file_index += 1 + datastream_file_names.append(temp_file_name) + temp_file_path = os.path.join(temp_dir, temp_file_name) + with open(temp_file_path, 'w') as csv_file: + for line in generate_csv(datastream): + csv_file.write(line) + archive_resource.file_upload(temp_file_path) + + return 201, archive_resource.resource_id diff --git a/core/migrations/0004_thing_hydroshare_archive_link_and_more.py b/core/migrations/0004_thing_hydroshare_archive_link_and_more.py new file mode 100644 index 00000000..1f9b180a --- /dev/null +++ b/core/migrations/0004_thing_hydroshare_archive_link_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.4 on 2023-12-01 18:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_unitchangelog_thingchangelog_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='thing', + name='hydroshare_archive_link', + field=models.CharField(blank=True, db_column='hydroshareArchiveLink', max_length=500, null=True), + ), + migrations.AddField( + model_name='thingchangelog', + name='hydroshare_archive_link', + field=models.CharField(blank=True, db_column='hydroshareArchiveLink', max_length=500, null=True), + ), + ] diff --git a/core/models.py b/core/models.py index 8514dcbb..546dd039 100644 --- a/core/models.py +++ b/core/models.py @@ -39,6 +39,7 @@ class Thing(models.Model): site_type = models.CharField(max_length=200, db_column='siteType') is_private = models.BooleanField(default=False, db_column='isPrivate') data_disclaimer = models.TextField(null=True, blank=True, db_column='dataDisclaimer') + hydroshare_archive_link = models.CharField(max_length=500, blank=True, null=True, db_column='hydroshareArchiveLink') location = models.OneToOneField(Location, related_name='thing', on_delete=models.CASCADE, db_column='locationId') history = HistoricalRecords(custom_model_name='ThingChangeLog', related_name='log') diff --git a/environment.yml b/environment.yml index 9d90e0c2..0c6cb40c 100644 --- a/environment.yml +++ b/environment.yml @@ -28,3 +28,5 @@ dependencies: - django_ses==2.5.0 - djangorestframework-simplejwt==5.3.0 - django-simple-history==3.4.0 + - hsclient==0.3.3 + - hsmodels==0.5.8 diff --git a/requirements.txt b/requirements.txt index 2b3ef251..cff60de1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,5 @@ Authlib==1.2.1 django-storages==1.14.2 djangorestframework-simplejwt==5.3.0 django-simple-history==3.4.0 +hsclient==0.3.3 +hsmodels==0.5.8