From 4f72903cee884bf934445903c2f2747f86038f20 Mon Sep 17 00:00:00 2001 From: Tobias Kampmann Date: Tue, 16 Jan 2024 15:02:42 +0100 Subject: [PATCH] feat(website,backend): adapt endpoint, and frontend, add tests, add contrains --- .../org/loculus/backend/api/DataUseTerms.kt | 109 ++++++++++++++++++ .../loculus/backend/api/SubmissionTypes.kt | 7 +- .../controller/DataUseTermsController.kt | 10 +- .../controller/SubmissionController.kt | 11 ++ .../org/loculus/backend/model/SubmitModel.kt | 4 +- .../DataUseTermsDatabaseService.kt | 44 +++---- .../datauseterms/DataUseTermsTables.kt | 4 +- .../submission/UploadDatabaseService.kt | 34 +++++- .../controller/ExceptionHandlerTest.kt | 5 + .../SingleSegmentedSubmitEndpointTest.kt | 22 ++-- .../submission/SubmissionControllerClient.kt | 5 + .../submission/SubmitEndpointTest.kt | 84 +++++++++++++- .../backend/service/DataUseTermsTest.kt | 83 +++++++++++++ website/src/components/DataUploadForm.tsx | 66 ++++++++++- website/src/components/Edit/EditPage.tsx | 2 +- .../Submission/SubmissionForm.spec.tsx | 27 ++++- .../components/Submission/SubmissionForm.tsx | 34 ++++-- website/src/components/User/GroupManager.tsx | 2 +- website/src/components/User/GroupPage.tsx | 2 +- .../UserSequenceList/SequenceEntryTable.tsx | 2 +- ...withQueryProvider.tsx => withProvider.tsx} | 13 +++ .../src/pages/[organism]/submit/index.astro | 3 +- website/src/types/backend.ts | 22 ++++ website/src/utils/DateTimeInMonths.tsx | 3 + website/tests/pages/submit/index.spec.ts | 23 +++- website/tests/pages/submit/submit.page.ts | 15 +++ website/tests/util/backendCalls.ts | 4 +- 27 files changed, 555 insertions(+), 85 deletions(-) create mode 100644 backend/src/main/kotlin/org/loculus/backend/api/DataUseTerms.kt create mode 100644 backend/src/test/kotlin/org/loculus/backend/service/DataUseTermsTest.kt rename website/src/components/common/{withQueryProvider.tsx => withProvider.tsx} (50%) create mode 100644 website/src/utils/DateTimeInMonths.tsx diff --git a/backend/src/main/kotlin/org/loculus/backend/api/DataUseTerms.kt b/backend/src/main/kotlin/org/loculus/backend/api/DataUseTerms.kt new file mode 100644 index 000000000..f0c1e0b91 --- /dev/null +++ b/backend/src/main/kotlin/org/loculus/backend/api/DataUseTerms.kt @@ -0,0 +1,109 @@ +package org.loculus.backend.api + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonPropertyOrder +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonTypeName +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit.Companion.YEAR +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.plus +import kotlinx.datetime.toLocalDateTime +import mu.KotlinLogging +import org.loculus.backend.config.logger +import org.loculus.backend.controller.BadRequestException + +enum class DataUseTermsType { + OPEN, + RESTRICTED, +} + +val logger = KotlinLogging.logger { } + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +@JsonSubTypes( + JsonSubTypes.Type(value = DataUseTerms.Open::class, name = "OPEN"), + JsonSubTypes.Type(value = DataUseTerms.Restricted::class, name = "RESTRICTED"), +) +@JsonPropertyOrder(value = ["type", "restrictedUntil", "changeDateTime"]) +sealed interface DataUseTerms { + val type: DataUseTermsType + + @JsonTypeName("OPEN") + data class Open(private val dummy: String = "") : + DataUseTerms { + @JsonIgnore + override val type = DataUseTermsType.OPEN + } + + @JsonTypeName("RESTRICTED") + data class Restricted( + @JsonSerialize(using = LocalDateSerializer::class) + val restrictedUntil: LocalDate, + ) : DataUseTerms { + @JsonIgnore + override val type = DataUseTermsType.RESTRICTED + } + + companion object { + fun fromParameters(type: DataUseTermsType, restrictedUntilString: String?): DataUseTerms { + logger.info { "Creating DataUseTerms from parameters: type=$type, restrictedUntil=$restrictedUntilString" } + return when (type) { + DataUseTermsType.OPEN -> Open() + DataUseTermsType.RESTRICTED -> { + val restrictedUntil = parseRestrictedUntil(restrictedUntilString) + validateRestrictedUntil(restrictedUntil) + Restricted(restrictedUntil) + } + } + } + + private fun parseRestrictedUntil(restrictedUntilString: String?): LocalDate { + if (restrictedUntilString == null) { + throw BadRequestException("The date 'restrictedUntil' must be set if 'dataUseTermsType' is RESTRICTED.") + } + return try { + LocalDate.parse(restrictedUntilString) + } catch (e: Exception) { + throw BadRequestException( + "The date 'restrictedUntil' must be a valid date in the format YYYY-MM-DD: $restrictedUntilString.", + ) + } + } + + private fun validateRestrictedUntil(restrictedUntil: LocalDate) { + val now = Clock.System.now().toLocalDateTime(TimeZone.UTC).date + val oneYearFromNow = now.plus(1, YEAR) + + if (restrictedUntil < now) { + throw BadRequestException( + "The date 'restrictedUntil' must be in the future, up to a maximum of 1 year from now.", + ) + } + if (restrictedUntil > oneYearFromNow) { + throw BadRequestException( + "The date 'restrictedUntil' must not exceed 1 year from today.", + ) + } + } + } +} + +class LocalDateSerializer : StdSerializer(LocalDate::class.java) { + override fun serialize(value: LocalDate, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(value.toString()) + } +} + +class LocalDateTimeSerializer : StdSerializer(LocalDateTime::class.java) { + override fun serialize(value: LocalDateTime, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(value.toString()) + } +} diff --git a/backend/src/main/kotlin/org/loculus/backend/api/SubmissionTypes.kt b/backend/src/main/kotlin/org/loculus/backend/api/SubmissionTypes.kt index efb59a979..6a32cb3dd 100644 --- a/backend/src/main/kotlin/org/loculus/backend/api/SubmissionTypes.kt +++ b/backend/src/main/kotlin/org/loculus/backend/api/SubmissionTypes.kt @@ -27,6 +27,7 @@ data class SubmissionIdMapping( override val accession: Accession, override val version: Version, val submissionId: String, + val dataUseTerms: DataUseTerms? = null, ) : AccessionVersionInterface fun List.toPairs() = map { Pair(it.accession, it.version) } @@ -148,12 +149,6 @@ data class SequenceEntryStatus( val isRevocation: Boolean = false, ) : AccessionVersionInterface -data class RevisedData( - val submissionId: String, - val accession: Accession, - val originalData: OriginalData, -) - data class UnprocessedData( @Schema(example = "123") override val accession: Accession, @Schema(example = "1") override val version: Version, diff --git a/backend/src/main/kotlin/org/loculus/backend/controller/DataUseTermsController.kt b/backend/src/main/kotlin/org/loculus/backend/controller/DataUseTermsController.kt index 36e0f2f3f..0ad0e66f8 100644 --- a/backend/src/main/kotlin/org/loculus/backend/controller/DataUseTermsController.kt +++ b/backend/src/main/kotlin/org/loculus/backend/controller/DataUseTermsController.kt @@ -3,12 +3,13 @@ package org.loculus.backend.controller import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.security.SecurityRequirement -import org.loculus.backend.service.datauseterms.DataUseTerms +import org.loculus.backend.api.DataUseTerms import org.loculus.backend.service.datauseterms.DataUseTermsDatabaseService import org.loculus.backend.utils.Accession import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @@ -27,9 +28,12 @@ class DataUseTermsController( @Parameter( description = "The accession of the dataset to set the data use terms for", ) @RequestParam accession: Accession, + @Parameter( + description = "The new data use terms", + ) @RequestBody newDataUseTerms: DataUseTerms, ) = dataUseTermsDatabaseService.setNewDataUseTerms( - accession, + listOf(accession), username, - DataUseTerms(), + DataUseTerms.Open(), ) } diff --git a/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionController.kt b/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionController.kt index 5a702d392..72ca5d76b 100644 --- a/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionController.kt +++ b/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionController.kt @@ -11,6 +11,8 @@ import jakarta.validation.Valid import jakarta.validation.constraints.Max import mu.KotlinLogging import org.loculus.backend.api.AccessionVersion +import org.loculus.backend.api.DataUseTerms +import org.loculus.backend.api.DataUseTermsType import org.loculus.backend.api.Organism import org.loculus.backend.api.ProcessedData import org.loculus.backend.api.SequenceEntryStatus @@ -67,6 +69,14 @@ class SubmissionController( @Parameter(description = GROUP_DESCRIPTION) @RequestParam groupName: String, @Parameter(description = METADATA_FILE_DESCRIPTION) @RequestParam metadataFile: MultipartFile, @Parameter(description = SEQUENCE_FILE_DESCRIPTION) @RequestParam sequenceFile: MultipartFile, + @Parameter(description = "Data Use terms under which data is released.") + @RequestParam + dataUseTermsType: DataUseTermsType, + @Parameter( + description = "Mandatory when data use terms are set to 'RESTRICTED'." + + " It is the date when the sequence entries will become 'OPEN'." + + " Format: YYYY-MM-DD", + ) @RequestParam restrictedUntil: String?, ): List { val params = SubmissionParams.OriginalSubmissionParams( organism, @@ -74,6 +84,7 @@ class SubmissionController( metadataFile, sequenceFile, groupName, + DataUseTerms.fromParameters(dataUseTermsType, restrictedUntil), ) return submitModel.processSubmissions(UUID.randomUUID().toString(), params) } diff --git a/backend/src/main/kotlin/org/loculus/backend/model/SubmitModel.kt b/backend/src/main/kotlin/org/loculus/backend/model/SubmitModel.kt index 8cd13b5be..6c8aa995a 100644 --- a/backend/src/main/kotlin/org/loculus/backend/model/SubmitModel.kt +++ b/backend/src/main/kotlin/org/loculus/backend/model/SubmitModel.kt @@ -7,6 +7,7 @@ import mu.KotlinLogging import org.apache.commons.compress.archivers.zip.ZipFile import org.apache.commons.compress.compressors.CompressorStreamFactory import org.jetbrains.exposed.exceptions.ExposedSQLException +import org.loculus.backend.api.DataUseTerms import org.loculus.backend.api.Organism import org.loculus.backend.api.SubmissionIdMapping import org.loculus.backend.controller.BadRequestException @@ -46,6 +47,7 @@ interface SubmissionParams { override val metadataFile: MultipartFile, override val sequenceFile: MultipartFile, val groupName: String, + val dataUseTerms: DataUseTerms, ) : SubmissionParams { override val uploadType: UploadType = UploadType.ORIGINAL } @@ -116,7 +118,7 @@ class SubmitModel( } log.debug { "Persisting submission with uploadId $uploadId" } - uploadDatabaseService.mapAndCopy(uploadId, submissionParams.uploadType) + uploadDatabaseService.mapAndCopy(uploadId, submissionParams) } finally { uploadDatabaseService.deleteUploadData(uploadId) } diff --git a/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsDatabaseService.kt index 8d930229d..495150ab3 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsDatabaseService.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsDatabaseService.kt @@ -1,50 +1,42 @@ package org.loculus.backend.service.datauseterms import kotlinx.datetime.Clock -import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import mu.KotlinLogging -import org.jetbrains.exposed.exceptions.ExposedSQLException -import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.batchInsert +import org.loculus.backend.api.DataUseTerms import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -enum class DataUseTermsType { - RESTRICTED, - OPEN, -} - -data class DataUseTerms( - val restrictedUntil: LocalDateTime? = null, - val changeDateTime: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.UTC), - val dataUseTermsType: DataUseTermsType = DataUseTermsType.OPEN, -) - private val log = KotlinLogging.logger { } @Service @Transactional class DataUseTermsDatabaseService { - fun setNewDataUseTerms(accession: String, username: String, newDataUseTerms: DataUseTerms) { + fun setNewDataUseTerms(accessions: List, username: String, newDataUseTerms: DataUseTerms) { log.info { - "Setting new data use terms for accession $accession. " + + "Setting new data use terms for accessions $accessions. " + "Just an entry in the new Table. " + "Will be filled with real juicy logic in the next tickets. See #760 ff. " } val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) - try { - DataUseTermsTable.insert { - it[accessionColumn] = accession - it[changeDateColumn] = now - it[dataUseTermsTypeColumn] = newDataUseTerms.dataUseTermsType - it[restrictedUntilColumn] = newDataUseTerms.restrictedUntil - it[userNameColumn] = username + + DataUseTermsTable.batchInsert(accessions) { + this[DataUseTermsTable.accessionColumn] = it + this[DataUseTermsTable.changeDateColumn] = now + this[DataUseTermsTable.dataUseTermsTypeColumn] = newDataUseTerms.type + this[DataUseTermsTable.restrictedUntilColumn] = when (newDataUseTerms) { + is DataUseTerms.Restricted -> { + newDataUseTerms.restrictedUntil + } + + else -> { + null + } } - } catch (e: ExposedSQLException) { - log.info("Error: ${e.sqlState}") - throw e + this[DataUseTermsTable.userNameColumn] = username } } } diff --git a/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsTables.kt b/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsTables.kt index 29c1540f6..53ed0b354 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsTables.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/datauseterms/DataUseTermsTables.kt @@ -1,7 +1,9 @@ package org.loculus.backend.service.datauseterms import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.kotlin.datetime.date import org.jetbrains.exposed.sql.kotlin.datetime.datetime +import org.loculus.backend.api.DataUseTermsType const val DATA_USE_TERMS_TABLE_NAME = "data_use_terms_table" @@ -9,6 +11,6 @@ object DataUseTermsTable : Table(DATA_USE_TERMS_TABLE_NAME) { val accessionColumn = text("accession") val changeDateColumn = datetime("change_date") val dataUseTermsTypeColumn = enumeration("data_use_terms_type", DataUseTermsType::class) - val restrictedUntilColumn = datetime("restricted_until").nullable() + val restrictedUntilColumn = date("restricted_until").nullable() val userNameColumn = text("user_name") } diff --git a/backend/src/main/kotlin/org/loculus/backend/service/submission/UploadDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/submission/UploadDatabaseService.kt index bdb575819..4c445dd74 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/submission/UploadDatabaseService.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/submission/UploadDatabaseService.kt @@ -15,7 +15,9 @@ import org.loculus.backend.api.Organism import org.loculus.backend.api.Status import org.loculus.backend.api.SubmissionIdMapping import org.loculus.backend.model.SubmissionId +import org.loculus.backend.model.SubmissionParams import org.loculus.backend.model.UploadType +import org.loculus.backend.service.datauseterms.DataUseTermsDatabaseService import org.loculus.backend.service.submission.MetadataUploadAuxTable.accessionColumn import org.loculus.backend.service.submission.MetadataUploadAuxTable.groupNameColumn import org.loculus.backend.service.submission.MetadataUploadAuxTable.metadataColumn @@ -43,6 +45,7 @@ class UploadDatabaseService( private val parseFastaHeader: ParseFastaHeader, private val compressor: CompressionService, private val submissionPreconditionValidator: SubmissionPreconditionValidator, + private val dataUseTermsDatabaseService: DataUseTermsDatabaseService, ) { fun batchInsertMetadataInAuxTable( @@ -112,11 +115,13 @@ class UploadDatabaseService( }, ) - fun mapAndCopy(uploadId: String, uploadType: UploadType): List = transaction { - log.debug { "mapping and copying sequences with UploadId $uploadId and uploadType: $uploadType" } + fun mapAndCopy(uploadId: String, submissionParams: SubmissionParams): List = transaction { + log.debug { + "mapping and copying sequences with UploadId $uploadId and uploadType: $submissionParams.uploadType" + } - exec( - generateMapAndCopyStatement(uploadType), + val insertionResult = exec( + generateMapAndCopyStatement(submissionParams.uploadType), listOf( Pair(VarCharColumnType(), uploadId), ), @@ -132,6 +137,27 @@ class UploadDatabaseService( } result.toList() } ?: emptyList() + + val result = if (submissionParams is SubmissionParams.OriginalSubmissionParams) { + dataUseTermsDatabaseService.setNewDataUseTerms( + insertionResult.map { it.accession }, + submissionParams.username, + submissionParams.dataUseTerms, + ) + + insertionResult.map { + SubmissionIdMapping( + it.accession, + it.version, + it.submissionId, + submissionParams.dataUseTerms, + ) + } + } else { + insertionResult + } + + return@transaction result } fun deleteUploadData(uploadId: String) { diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/ExceptionHandlerTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/ExceptionHandlerTest.kt index acd8de455..eff49858c 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/ExceptionHandlerTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/ExceptionHandlerTest.kt @@ -8,6 +8,7 @@ import org.hamcrest.Matchers.containsString import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.loculus.backend.SpringBootTestWithoutDatabase +import org.loculus.backend.api.DataUseTermsType import org.loculus.backend.api.SubmissionIdMapping import org.loculus.backend.model.SubmitModel import org.springframework.beans.factory.annotation.Autowired @@ -28,6 +29,7 @@ private val validRequest: MockHttpServletRequestBuilder = multipart(validRoute) .file("sequenceFile", "sequences".toByteArray()) .file("metadataFile", "metadata".toByteArray()) .param("groupName", "groupName") + .param("dataUseTermsType", DataUseTermsType.OPEN.name) .withAuth() private val validResponse = emptyList() @@ -49,6 +51,8 @@ class ExceptionHandlerTest(@Autowired val mockMvc: MockMvc) { any(), any(), any(), + any(), + any(), ) @Test @@ -124,6 +128,7 @@ class ExceptionHandlerWithMockedModelTest(@Autowired val mockMvc: MockMvc) { .file("sequenceFile", "sequences".toByteArray()) .file("metadataFile", "metadata".toByteArray()) .param("groupName", "groupName") + .param("dataUseTermsType", DataUseTermsType.OPEN.name) .withAuth(), ) .andExpect(status().isBadRequest) diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SingleSegmentedSubmitEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SingleSegmentedSubmitEndpointTest.kt index adb8b9d55..d3966bc59 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SingleSegmentedSubmitEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SingleSegmentedSubmitEndpointTest.kt @@ -29,17 +29,17 @@ class SingleSegmentedSubmitEndpointTest( submissionControllerClient.submit( SubmitFiles.metadataFileWith( content = """ - submissionId firstColumn - header1 someValue - header2 someValue + submissionId firstColumn + header1 someValue + header2 someValue """.trimIndent(), ), SubmitFiles.sequenceFileWith( content = """ - >header1 - AC - >header2 - GT + >header1 + AC + >header2 + GT """.trimIndent(), ), ) @@ -64,14 +64,14 @@ class SingleSegmentedSubmitEndpointTest( submissionControllerClient.submit( SubmitFiles.metadataFileWith( content = """ - submissionId firstColumn - header1 someValue + submissionId firstColumn + header1 someValue """.trimIndent(), ), SubmitFiles.sequenceFileWith( content = """ - >header1_$DEFAULT_SEQUENCE_NAME - AC + >header1_$DEFAULT_SEQUENCE_NAME + AC """.trimIndent(), ), ) diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionControllerClient.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionControllerClient.kt index 2930c4228..3194cded3 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionControllerClient.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionControllerClient.kt @@ -2,6 +2,7 @@ package org.loculus.backend.controller.submission import com.fasterxml.jackson.databind.ObjectMapper import org.loculus.backend.api.AccessionVersion +import org.loculus.backend.api.DataUseTermsType import org.loculus.backend.api.SubmittedProcessedData import org.loculus.backend.api.UnprocessedData import org.loculus.backend.controller.DEFAULT_GROUP_NAME @@ -29,12 +30,16 @@ class SubmissionControllerClient(private val mockMvc: MockMvc, private val objec sequencesFile: MockMultipartFile, organism: String = DEFAULT_ORGANISM, groupName: String = DEFAULT_GROUP_NAME, + dataUseTermType: DataUseTermsType = DataUseTermsType.OPEN, + restrictedUntil: String? = null, jwt: String? = jwtForDefaultUser, ): ResultActions = mockMvc.perform( multipart(addOrganismToPath("/submit", organism = organism)) .file(sequencesFile) .file(metadataFile) .param("groupName", groupName) + .param("dataUseTermsType", dataUseTermType.name) + .param("restrictedUntil", restrictedUntil) .withAuth(jwt), ) diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEndpointTest.kt index f099eccef..185963d88 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitEndpointTest.kt @@ -1,10 +1,18 @@ package org.loculus.backend.controller.submission +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit.Companion.DAY +import kotlinx.datetime.DateTimeUnit.Companion.YEAR +import kotlinx.datetime.TimeZone +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import kotlinx.datetime.toLocalDateTime import org.hamcrest.Matchers.containsString import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource +import org.loculus.backend.api.DataUseTermsType import org.loculus.backend.api.Organism import org.loculus.backend.controller.DEFAULT_GROUP_NAME import org.loculus.backend.controller.DEFAULT_ORGANISM @@ -114,14 +122,14 @@ class SubmitEndpointTest( submissionControllerClient.submit( SubmitFiles.metadataFileWith( content = """ - submissionId firstColumn - commonHeader someValue + submissionId firstColumn + commonHeader someValue """.trimIndent(), ), SubmitFiles.sequenceFileWith( content = """ - >commonHeader_nonExistingSegmentName - AC + >commonHeader_nonExistingSegmentName + AC """.trimIndent(), ), organism = OTHER_ORGANISM, @@ -141,14 +149,23 @@ class SubmitEndpointTest( expectedTitle: String, expectedMessage: String, organism: Organism, + dataUseTermType: DataUseTermsType, + restrictedUntil: String?, ) { - submissionControllerClient.submit(metadataFile, sequencesFile, organism = organism.name) + submissionControllerClient.submit( + metadataFile, + sequencesFile, + organism = organism.name, + dataUseTermType = dataUseTermType, + restrictedUntil = restrictedUntil, + ) .andExpect(expectedStatus) .andExpect(jsonPath("\$.title").value(expectedTitle)) .andExpect(jsonPath("\$.detail", containsString(expectedMessage))) } companion object { + @JvmStatic fun compressionForSubmit(): List { return listOf( @@ -179,6 +196,8 @@ class SubmitEndpointTest( @JvmStatic fun badRequestForSubmit(): List { + val now = Clock.System.now().toLocalDateTime(TimeZone.UTC).date + return listOf( Arguments.of( "metadata file with wrong submitted filename", @@ -188,6 +207,8 @@ class SubmitEndpointTest( "Bad Request", "Required part 'metadataFile' is not present.", DEFAULT_ORGANISM, + DataUseTermsType.OPEN, + null, ), Arguments.of( "sequences file with wrong submitted filename", @@ -197,6 +218,8 @@ class SubmitEndpointTest( "Bad Request", "Required part 'sequenceFile' is not present.", DEFAULT_ORGANISM, + DataUseTermsType.OPEN, + null, ), Arguments.of( "wrong extension for metadata file", @@ -210,6 +233,8 @@ class SubmitEndpointTest( ".${metadataFileTypes.getCompressedExtensions()} " + "for compressed submissions", DEFAULT_ORGANISM, + DataUseTermsType.OPEN, + null, ), Arguments.of( "wrong extension for sequences file", @@ -223,6 +248,8 @@ class SubmitEndpointTest( ".${sequenceFileTypes.getCompressedExtensions()} " + "for compressed submissions", DEFAULT_ORGANISM, + DataUseTermsType.OPEN, + null, ), Arguments.of( "metadata file where one row has a blank header", @@ -238,6 +265,8 @@ class SubmitEndpointTest( "Unprocessable Entity", "A row in metadata file contains no submissionId", DEFAULT_ORGANISM, + DataUseTermsType.OPEN, + null, ), Arguments.of( "metadata file with no header", @@ -252,6 +281,8 @@ class SubmitEndpointTest( "Unprocessable Entity", "The metadata file does not contain the header 'submissionId'", DEFAULT_ORGANISM, + DataUseTermsType.OPEN, + null, ), Arguments.of( "duplicate headers in metadata file", @@ -267,6 +298,8 @@ class SubmitEndpointTest( "Unprocessable Entity", "Metadata file contains at least one duplicate submissionId", DEFAULT_ORGANISM, + DataUseTermsType.OPEN, + null, ), Arguments.of( "duplicate headers in sequence file", @@ -283,6 +316,8 @@ class SubmitEndpointTest( "Unprocessable Entity", "Sequence file contains at least one duplicate submissionId", DEFAULT_ORGANISM, + DataUseTermsType.OPEN, + null, ), Arguments.of( "metadata file misses headers", @@ -304,6 +339,8 @@ class SubmitEndpointTest( "Unprocessable Entity", "Sequence file contains 1 submissionIds that are not present in the metadata file: notInMetadata", DEFAULT_ORGANISM, + DataUseTermsType.OPEN, + null, ), Arguments.of( "sequence file misses headers", @@ -324,6 +361,8 @@ class SubmitEndpointTest( "Unprocessable Entity", "Metadata file contains 1 submissionIds that are not present in the sequence file: notInSequences", DEFAULT_ORGANISM, + DataUseTermsType.OPEN, + null, ), Arguments.of( "FASTA header misses segment name", @@ -344,6 +383,41 @@ class SubmitEndpointTest( "The FASTA header commonHeader does not contain the segment name. Please provide the segment " + "name in the format _", OTHER_ORGANISM, + DataUseTermsType.OPEN, + null, + ), + Arguments.of( + "restricted use data without until date", + DefaultFiles.metadataFile, + DefaultFiles.sequencesFile, + status().isBadRequest, + "Bad Request", + "The date 'restrictedUntil' must be set if 'dataUseTermsType' is RESTRICTED.", + DEFAULT_ORGANISM, + DataUseTermsType.RESTRICTED, + null, + ), + Arguments.of( + "restricted use data with until date in the past", + DefaultFiles.metadataFile, + DefaultFiles.sequencesFile, + status().isBadRequest, + "Bad Request", + "The date 'restrictedUntil' must be in the future, up to a maximum of 1 year from now.", + DEFAULT_ORGANISM, + DataUseTermsType.RESTRICTED, + now.minus(1, DAY).toString(), + ), + Arguments.of( + "restricted use data with until date further than 1 year", + DefaultFiles.metadataFile, + DefaultFiles.sequencesFile, + status().isBadRequest, + "Bad Request", + "The date 'restrictedUntil' must not exceed 1 year from today.", + DEFAULT_ORGANISM, + DataUseTermsType.RESTRICTED, + now.plus(2, YEAR).toString(), ), ) } diff --git a/backend/src/test/kotlin/org/loculus/backend/service/DataUseTermsTest.kt b/backend/src/test/kotlin/org/loculus/backend/service/DataUseTermsTest.kt new file mode 100644 index 000000000..fa2bad8df --- /dev/null +++ b/backend/src/test/kotlin/org/loculus/backend/service/DataUseTermsTest.kt @@ -0,0 +1,83 @@ +package org.loculus.backend.service + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import kotlinx.datetime.LocalDate +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.MatcherAssert.assertThat +import org.junit.jupiter.api.Test +import org.loculus.backend.SpringBootTestWithoutDatabase +import org.loculus.backend.api.DataUseTerms +import org.springframework.beans.factory.annotation.Autowired + +@SpringBootTestWithoutDatabase +class DataUseTermsTest(@Autowired private val objectMapper: ObjectMapper) { + + @Test + fun `deserialize restricted`() { + val restrictedUntil = "2021-02-01" + + val dataUseTerms = objectMapper.readValue( + """ + { + "type": "RESTRICTED", + "restrictedUntil": "$restrictedUntil" + } + """.replace("\n", "").replace(" ", ""), + ) + + assertThat( + dataUseTerms, + `is`(DataUseTerms.Restricted(LocalDate.parse(restrictedUntil))), + ) + } + + @Test + fun `deserialize open`() { + val dataUseTerms = objectMapper.readValue( + """ + { + "type": "OPEN" + } + """.replace("\n", "").replace(" ", ""), + ) + + assertThat(dataUseTerms, `is`(DataUseTerms.Open())) + } + + @Test + fun `serialized restricted`() { + val restrictedUntil = "2021-02-01" + + val dataUseTerms = DataUseTerms.Restricted(LocalDate.parse(restrictedUntil)) + + val expected = """ + { + "type" : "RESTRICTED", + "restrictedUntil" : "$restrictedUntil" + } + """ + + assertThat( + objectMapper.readTree(objectMapper.writeValueAsString(dataUseTerms)), + equalTo(objectMapper.readTree(expected)), + ) + } + + @Test + fun `serialized open`() { + val dataUseTerms = DataUseTerms.Open() + + val expected = """ + { + "type" : "OPEN" + } + """ + + assertThat( + objectMapper.readTree(objectMapper.writeValueAsString(dataUseTerms)), + equalTo(objectMapper.readTree(expected)), + ) + } +} diff --git a/website/src/components/DataUploadForm.tsx b/website/src/components/DataUploadForm.tsx index fe99412fe..b6fa85fb0 100644 --- a/website/src/components/DataUploadForm.tsx +++ b/website/src/components/DataUploadForm.tsx @@ -1,16 +1,25 @@ import { CircularProgress, TextField } from '@mui/material'; +import { DatePicker } from '@mui/x-date-pickers'; import { isErrorFromAlias } from '@zodios/core'; import type { AxiosError } from 'axios'; +import { type DateTime } from 'luxon'; import { type ChangeEvent, type FormEvent, useMemo, useState } from 'react'; -import { withQueryProvider } from './common/withQueryProvider.tsx'; +import { withLocalizationProvider, withQueryProvider } from './common/withProvider.tsx'; import { getClientLogger } from '../clientLogger.ts'; import { useGroupManagementClient } from '../hooks/useGroupOperations.ts'; import { routes } from '../routes.ts'; import { backendApi } from '../services/backendApi.ts'; import { backendClientHooks } from '../services/serviceHooks.ts'; -import type { SubmissionIdMapping } from '../types/backend.ts'; +import { + type DataUseTermsType, + dataUseTermsTypes, + openDataUseTermsType, + restrictedDataUseTermsType, + type SubmissionIdMapping, +} from '../types/backend.ts'; import type { ClientConfig } from '../types/runtimeConfig.ts'; +import { dateTimeInMonths } from '../utils/DateTimeInMonths.tsx'; import { createAuthorizationHeader } from '../utils/createAuthorizationHeader.ts'; import { stringifyMaybeAxiosError } from '../utils/stringifyMaybeAxiosError.ts'; @@ -54,6 +63,8 @@ const InnerDataUploadForm = ({ const { submit, revise, isLoading } = useSubmitFiles(accessToken, organism, clientConfig, onSuccess, onError); const [selectedGroup, setSelectedGroup] = useState(undefined); + const [dataUseTermsType, setDataUseTermsType] = useState(openDataUseTermsType); + const [restrictedUntil, setRestrictedUntil] = useState(dateTimeInMonths(6)); const handleLoadExampleData = async () => { const { metadataFileContent, revisedMetadataFileContent, sequenceFileContent } = getExampleData(); @@ -86,7 +97,14 @@ const InnerDataUploadForm = ({ onError('Please select a group'); return; } - submit({ metadataFile, sequenceFile, groupName }); + submit({ + metadataFile, + sequenceFile, + groupName, + dataUseTermsType, + restrictedUntil: + dataUseTermsType === restrictedDataUseTermsType ? restrictedUntil.toFormat('yyyy-MM-dd') : null, + }); break; case 'revise': revise({ metadataFile, sequenceFile }); @@ -153,6 +171,44 @@ const InnerDataUploadForm = ({ }} /> + {action === 'submit' && ( + <> +
+ Data Use Terms + +
+ + (date !== null ? setRestrictedUntil(date) : null)} + /> + + )} +