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 f1e2ddfb00..0050c654ba 100644 --- a/backend/src/main/kotlin/org/loculus/backend/api/SubmissionTypes.kt +++ b/backend/src/main/kotlin/org/loculus/backend/api/SubmissionTypes.kt @@ -119,7 +119,9 @@ data class SequenceEntryVersionToEdit( override val version: Version, val status: Status, val groupId: Int, - val processedData: ProcessedData, + val isRevocation: Boolean = false, + @Schema(description = "Null for revocations, which are not preprocessed.") + val processedData: ProcessedData? = null, val originalData: OriginalData, @Schema(description = "The preprocessing will be considered failed if this is not empty") val errors: List? = null, 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 370e897e42..3e77efa179 100644 --- a/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionController.kt +++ b/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionController.kt @@ -339,14 +339,17 @@ open class SubmissionController( return ResponseEntity.ok().headers(headers).body(streamBody) } - @Operation(description = GET_DATA_TO_EDIT_SEQUENCE_VERSION_DESCRIPTION) - @GetMapping("/get-data-to-edit/{accession}/{version}", produces = [MediaType.APPLICATION_JSON_VALUE]) - fun getSequenceEntryVersionToEdit( + @Operation(description = GET_ORIGINAL_DATA_FOR_ENTRY_DESCRIPTION) + @GetMapping( + "/get-original-data-for-entry/{accession}/{version}", + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + fun getOriginalDataForEntry( @PathVariable @Valid organism: Organism, @PathVariable accession: Accession, @PathVariable version: Long, @HiddenParam authenticatedUser: AuthenticatedUser, - ): SequenceEntryVersionToEdit = submissionDatabaseService.getSequenceEntryVersionToEdit( + ): SequenceEntryVersionToEdit = submissionDatabaseService.getOriginalDataForEntry( authenticatedUser, AccessionVersion(accession, version), organism, diff --git a/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionControllerDescriptions.kt b/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionControllerDescriptions.kt index 02a62a1513..ff54ac909d 100644 --- a/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionControllerDescriptions.kt +++ b/backend/src/main/kotlin/org/loculus/backend/controller/SubmissionControllerDescriptions.kt @@ -86,8 +86,10 @@ The submitted external data cannot be written to the database, e.g. if the acces state, if the pipeline submits invalid data or if the name of external metadata updater is not known. Rolls back the whole transaction. """ -const val GET_DATA_TO_EDIT_SEQUENCE_VERSION_DESCRIPTION = """ -Get original data for a single accession version for subsequent editing and edit/revision. +const val GET_ORIGINAL_DATA_FOR_ENTRY_DESCRIPTION = """ +Get original data for a single accession version. Used both for displaying read-only details of an entry +(including for revocations) and as the basis for subsequent editing or revising. +For revocations, no processed data is returned. """ const val GET_SEQUENCES_DESCRIPTION = """ diff --git a/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt index d7f6811b7f..dcf7956caf 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt @@ -1150,14 +1150,14 @@ class SubmissionDatabaseService( ) } - fun getSequenceEntryVersionToEdit( + fun getOriginalDataForEntry( authenticatedUser: AuthenticatedUser, accessionVersion: AccessionVersion, organism: Organism, ): SequenceEntryVersionToEdit { log.info { - "Getting sequence entry ${accessionVersion.displayAccessionVersion()} " + - "by ${authenticatedUser.username} to edit" + "Getting original data for sequence entry ${accessionVersion.displayAccessionVersion()} " + + "by ${authenticatedUser.username}" } accessionPreconditionValidator.validate { @@ -1181,21 +1181,16 @@ class SubmissionDatabaseService( .where { SequenceEntriesView.accessionVersionEquals(accessionVersion) } .first() - if (selectedSequenceEntry[SequenceEntriesView.isRevocationColumn]) { - throw UnprocessableEntityException( - "Accession version ${accessionVersion.displayAccessionVersion()} is a revocation.", - ) - } + val processedDataValue = selectedSequenceEntry[SequenceEntriesView.processedDataColumn] + ?.let { processedDataPostprocessor.retrieveFromStoredValue(it, organism) } return SequenceEntryVersionToEdit( accession = selectedSequenceEntry[SequenceEntriesView.accessionColumn], version = selectedSequenceEntry[SequenceEntriesView.versionColumn], status = Status.fromString(selectedSequenceEntry[SequenceEntriesView.statusColumn]), groupId = selectedSequenceEntry[SequenceEntriesView.groupIdColumn], - processedData = processedDataPostprocessor.retrieveFromStoredValue( - selectedSequenceEntry[SequenceEntriesView.processedDataColumn]!!, - organism, - ), + isRevocation = selectedSequenceEntry[SequenceEntriesView.isRevocationColumn], + processedData = processedDataValue, originalData = compressionService.decompressSequencesInOriginalData( selectedSequenceEntry[SequenceEntriesView.unprocessedDataColumn]!!, ), diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetDataToEditEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetOriginalDataForEntryEndpointTest.kt similarity index 77% rename from backend/src/test/kotlin/org/loculus/backend/controller/submission/GetDataToEditEndpointTest.kt rename to backend/src/test/kotlin/org/loculus/backend/controller/submission/GetOriginalDataForEntryEndpointTest.kt index 61e6d48244..98675eaff0 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetDataToEditEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/GetOriginalDataForEntryEndpointTest.kt @@ -2,6 +2,7 @@ package org.loculus.backend.controller.submission import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat import org.junit.jupiter.api.Test import org.loculus.backend.api.Status @@ -21,7 +22,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPat import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @EndpointTest -class GetDataToEditEndpointTest( +class GetOriginalDataForEntryEndpointTest( @Autowired val client: SubmissionControllerClient, @Autowired val convenienceClient: SubmissionConvenienceClient, ) { @@ -29,7 +30,7 @@ class GetDataToEditEndpointTest( @Test fun `GIVEN invalid authorization token THEN returns 401 Unauthorized`() { expectUnauthorizedResponse { - client.getSequenceEntryToEdit( + client.getOriginalDataForEntry( "ShouldNotMatterAtAll", 1, jwt = it, @@ -47,7 +48,7 @@ class GetDataToEditEndpointTest( .assertStatusIs(Status.PROCESSED) .assertHasError(true) - val editedData = convenienceClient.getSequenceEntryToEdit( + val editedData = convenienceClient.getOriginalDataForEntry( accession = firstAccession, version = 1, ) @@ -61,7 +62,7 @@ class GetDataToEditEndpointTest( fun `WHEN I query data for a non-existent accession THEN refuses request with not found`() { val nonExistentAccession = "DefinitelyNotExisting" - client.getSequenceEntryToEdit(nonExistentAccession, 1) + client.getOriginalDataForEntry(nonExistentAccession, 1) .andExpect(status().isUnprocessableEntity) .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) .andExpect( @@ -79,9 +80,9 @@ class GetDataToEditEndpointTest( organism = DEFAULT_ORGANISM, ).first().accession - client.getSequenceEntryToEdit(firstAccession, 1, organism = DEFAULT_ORGANISM) + client.getOriginalDataForEntry(firstAccession, 1, organism = DEFAULT_ORGANISM) .andExpect(status().isOk) - client.getSequenceEntryToEdit(firstAccession, 1, organism = OTHER_ORGANISM) + client.getOriginalDataForEntry(firstAccession, 1, organism = OTHER_ORGANISM) .andExpect(status().isUnprocessableEntity) .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) .andExpect( @@ -99,7 +100,7 @@ class GetDataToEditEndpointTest( convenienceClient.prepareDataTo(Status.PROCESSED, errors = true) - client.getSequenceEntryToEdit("1", nonExistentAccessionVersion) + client.getOriginalDataForEntry("1", nonExistentAccessionVersion) .andExpect(status().isUnprocessableEntity) .andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON)) .andExpect( @@ -114,7 +115,7 @@ class GetDataToEditEndpointTest( val firstAccession = convenienceClient.prepareDataTo(Status.PROCESSED, errors = true).first().accession val userNameThatDoesNotHavePermissionToQuery = "theOneWhoMustNotBeNamed" - client.getSequenceEntryToEdit( + client.getOriginalDataForEntry( accession = firstAccession, version = 1, jwt = generateJwtFor(userNameThatDoesNotHavePermissionToQuery), @@ -135,7 +136,7 @@ class GetDataToEditEndpointTest( ) .first() - client.getSequenceEntryToEdit( + client.getOriginalDataForEntry( accession = accessionVersion.accession, version = accessionVersion.version, jwt = jwtForSuperUser, @@ -146,12 +147,23 @@ class GetDataToEditEndpointTest( } @Test - fun `GIVEN revocation version awaiting approval THEN throws unprocessable entity error`() { - val accessionVersion = convenienceClient.prepareDefaultSequenceEntriesToAwaitingApprovalForRevocation() - .first() + fun `GIVEN revocation awaiting approval THEN returns original data with version comment and null processed data`() { + val versionComment = "revocation reason for test" + val approvedAccession = convenienceClient.prepareDefaultSequenceEntriesToApprovedForRelease().first().accession + val accessionVersion = convenienceClient.revokeSequenceEntries( + listOf(approvedAccession), + versionComment = versionComment, + ).first() + + val originalData = convenienceClient.getOriginalDataForEntry( + accession = accessionVersion.accession, + version = accessionVersion.version, + ) - client.getSequenceEntryToEdit(accessionVersion.accession, accessionVersion.version) - .andExpect(status().isUnprocessableEntity) - .andExpect(jsonPath("\$.detail", containsString("is a revocation"))) + assertThat(originalData.accession, `is`(accessionVersion.accession)) + assertThat(originalData.version, `is`(accessionVersion.version)) + assertThat(originalData.isRevocation, `is`(true)) + assertThat(originalData.processedData, `is`(nullValue())) + assertThat(originalData.originalData.metadata["versionComment"], `is`(versionComment)) } } 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 c3b055ecd1..5210d6bd5d 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 @@ -172,14 +172,14 @@ class SubmissionControllerClient(private val mockMvc: MockMvc, private val objec .param("size", size?.toString()), ) - fun getSequenceEntryToEdit( + fun getOriginalDataForEntry( accession: Accession, version: Long, organism: String = DEFAULT_ORGANISM, groupName: String = DEFAULT_GROUP_NAME, jwt: String? = jwtForDefaultUser, ): ResultActions = mockMvc.perform( - get(addOrganismToPath("/get-data-to-edit/$accession/$version", organism = organism)) + get(addOrganismToPath("/get-original-data-for-entry/$accession/$version", organism = organism)) .withAuth(jwt) .param("groupName", groupName), ) diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionConvenienceClient.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionConvenienceClient.kt index c27dccc28b..a7c8c534eb 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionConvenienceClient.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmissionConvenienceClient.kt @@ -351,12 +351,12 @@ class SubmissionConvenienceClient( ?: error("Did not find $accession.$version for $userName") } - fun getSequenceEntryToEdit( + fun getOriginalDataForEntry( accession: Accession, version: Long, userName: String = DEFAULT_USER_NAME, ): SequenceEntryVersionToEdit = deserializeJsonResponse( - client.getSequenceEntryToEdit( + client.getOriginalDataForEntry( accession = accession, version = version, jwt = generateJwtFor(userName), diff --git a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitProcessedDataEndpointTest.kt b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitProcessedDataEndpointTest.kt index bfa72ea650..9285ef8d73 100644 --- a/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitProcessedDataEndpointTest.kt +++ b/backend/src/test/kotlin/org/loculus/backend/controller/submission/SubmitProcessedDataEndpointTest.kt @@ -105,12 +105,16 @@ class SubmitProcessedDataEndpointTest( convenienceClient.getSequenceEntry(accession = accessions.first(), version = 1) .assertStatusIs(Status.PROCESSED) - val sequenceEntryToEdit = convenienceClient.getSequenceEntryToEdit(accession = accessions.first(), version = 1) - assertThat(sequenceEntryToEdit.processedData.metadata, hasEntry("qc", DoubleNode(0.987654321))) - assertThat(sequenceEntryToEdit.processedData.metadata, hasEntry("age", IntNode(42))) - assertThat(sequenceEntryToEdit.processedData.metadata, hasEntry("region", TextNode("Europe"))) - assertThat(sequenceEntryToEdit.processedData.metadata, hasEntry("pangoLineage", TextNode("XBB.1.5"))) - assertThat(sequenceEntryToEdit.processedData.metadata, hasEntry("booleanColumn", BooleanNode.TRUE)) + val sequenceEntryToEdit = convenienceClient.getOriginalDataForEntry( + accession = accessions.first(), + version = 1, + ) + val metadata = sequenceEntryToEdit.processedData!!.metadata + assertThat(metadata, hasEntry("qc", DoubleNode(0.987654321))) + assertThat(metadata, hasEntry("age", IntNode(42))) + assertThat(metadata, hasEntry("region", TextNode("Europe"))) + assertThat(metadata, hasEntry("pangoLineage", TextNode("XBB.1.5"))) + assertThat(metadata, hasEntry("booleanColumn", BooleanNode.TRUE)) } @Test @@ -135,8 +139,8 @@ class SubmitProcessedDataEndpointTest( ) .andExpect(status().isNoContent) - val processedData = convenienceClient.getSequenceEntryToEdit(accession = accession, version = version) - .processedData + val processedData = convenienceClient.getOriginalDataForEntry(accession = accession, version = version) + .processedData!! assertThat(processedData.unalignedNucleotideSequences, hasEntry(MAIN_SEGMENT, "NACTG")) assertThat( diff --git a/cli/src/loculus_cli/utils/review_utils.py b/cli/src/loculus_cli/utils/review_utils.py index 7b93a571ac..94caf8615d 100644 --- a/cli/src/loculus_cli/utils/review_utils.py +++ b/cli/src/loculus_cli/utils/review_utils.py @@ -189,7 +189,7 @@ def get_sequence_details( backend_url = self.instance_config.backend_url response = httpx.get( - f"{backend_url}/{organism}/get-data-to-edit/{accession}/{version}", + f"{backend_url}/{organism}/get-original-data-for-entry/{accession}/{version}", headers=self._get_auth_headers(), ) response.raise_for_status() diff --git a/integration-tests/tests/specs/backend/authentication.spec.ts b/integration-tests/tests/specs/backend/authentication.spec.ts index 1f6124616c..4da673b595 100644 --- a/integration-tests/tests/specs/backend/authentication.spec.ts +++ b/integration-tests/tests/specs/backend/authentication.spec.ts @@ -47,7 +47,7 @@ function getBackendBaseUrl(): URL { test.describe('Backend authentication', () => { test('rejects tokens that were not signed by Keycloak', async ({ backendRequest }) => { - const response = await backendRequest.get('/dummy-organism/get-data-to-edit/1/1', { + const response = await backendRequest.get('/dummy-organism/get-original-data-for-entry/1/1', { headers: { Authorization: `Bearer ${tokenSignedWithDifferentKey}`, }, diff --git a/website/src/components/Edit/EditableSequences.ts b/website/src/components/Edit/EditableSequences.ts index 72d1272bb2..ff9e2d88cb 100644 --- a/website/src/components/Edit/EditableSequences.ts +++ b/website/src/components/Edit/EditableSequences.ts @@ -68,6 +68,9 @@ export class EditableSequences { */ static fromInitialData(initialData: SequenceEntryToEdit, maxSequencesPerEntry?: number): EditableSequences { const maxNumberRows = maxSequencesPerEntry ?? Infinity; + if (initialData.processedData === null) { + throw new Error('Cannot edit a sequence entry without processed data (e.g. a revocation)'); + } const fastaHeaderMap = EditableSequences.invertRecordMulti(initialData.processedData.sequenceNameToFastaId); const existingDataRows = Object.entries(initialData.originalData.unalignedNucleotideSequences).map( ([key, value]) => { diff --git a/website/src/components/ReviewPage/FilesDialog.tsx b/website/src/components/ReviewPage/FilesDialog.tsx index c5ae8d7da8..4f1a27f1ee 100644 --- a/website/src/components/ReviewPage/FilesDialog.tsx +++ b/website/src/components/ReviewPage/FilesDialog.tsx @@ -10,7 +10,7 @@ type FilesDialogProps = { }; export const FilesDialog: FC = ({ isOpen, onClose, dataToView }) => { - if (!isOpen || !dataToView) return null; + if (!isOpen || !dataToView?.processedData) return null; return (
diff --git a/website/src/components/ReviewPage/ReviewCard.tsx b/website/src/components/ReviewPage/ReviewCard.tsx index 202a35d6e5..85db1a869f 100644 --- a/website/src/components/ReviewPage/ReviewCard.tsx +++ b/website/src/components/ReviewPage/ReviewCard.tsx @@ -63,7 +63,11 @@ export const ReviewCard: FC = ({ const [isSequencesDialogOpen, setSequencesDialogOpen] = useState(false); const [isFilesDialogOpen, setFilesDialogOpen] = useState(false); const { isLoading, data } = useGetMetadataAndAnnotations(organism, clientConfig, accessToken, sequenceEntryStatus); - const hasFiles = Object.entries(data?.processedData.files ?? {}).length > 0; + const hasFiles = Object.entries(data?.processedData?.files ?? {}).length > 0; + const revocationVersionComment = + sequenceEntryStatus.isRevocation && typeof data?.originalData.metadata.versionComment === 'string' + ? data.originalData.metadata.versionComment + : undefined; const notProcessed = sequenceEntryStatus.status !== processedStatus; @@ -83,7 +87,7 @@ export const ReviewCard: FC = ({ keyName={getAccessionVersionString(sequenceEntryStatus)} value={sequenceEntryStatus.submissionId} /> - {data !== undefined && ( + {data?.processedData != null && ( )} {sequenceEntryStatus.isRevocation && ( @@ -95,14 +99,26 @@ export const ReviewCard: FC = ({ disableTruncate /> )} + {revocationVersionComment !== undefined && ( + + )}
setSequencesDialogOpen(true) : undefined} - viewFiles={data && !notProcessed ? () => setFilesDialogOpen(true) : undefined} + viewSequences={ + data?.processedData != null && !notProcessed ? () => setSequencesDialogOpen(true) : undefined + } + viewFiles={ + data?.processedData != null && !notProcessed ? () => setFilesDialogOpen(true) : undefined + } filesEnabled={filesEnabled} hasFiles={hasFiles} /> @@ -259,9 +275,11 @@ type MetadataListProps = { const isAnnotationPresent = (metadataField: string) => (item: ProcessingAnnotation) => item.processedFields[0].name === metadataField; -const MetadataList: FC = ({ data, isLoading, metadataDisplayNames }) => - !isLoading && - Object.entries(data.processedData.metadata).map(([metadataName, value], index) => +const MetadataList: FC = ({ data, isLoading, metadataDisplayNames }) => { + if (isLoading || data.processedData === null) { + return null; + } + return Object.entries(data.processedData.metadata).map(([metadataName, value], index) => value === null ? null : ( = ({ data, isLoading, metadataDisplayN /> ), ); +}; type ErrorsProps = { errors: ProcessingAnnotation[]; @@ -519,14 +538,14 @@ function useGetMetadataAndAnnotations( accessToken: string, sequenceEntryStatus: SequenceEntryStatus, ) { - const { status, accession, version, isRevocation } = sequenceEntryStatus; - return backendClientHooks(clientConfig).useGetDataToEdit( + const { status, accession, version } = sequenceEntryStatus; + return backendClientHooks(clientConfig).useGetOriginalDataForEntry( { headers: createAuthorizationHeader(accessToken), params: { organism, accession, version }, }, { - enabled: status !== receivedStatus && status !== inProcessingStatus && !isRevocation, + enabled: status !== receivedStatus && status !== inProcessingStatus, }, ); } diff --git a/website/src/components/ReviewPage/ReviewPage.spec.tsx b/website/src/components/ReviewPage/ReviewPage.spec.tsx index e872ec9102..072938b7fd 100644 --- a/website/src/components/ReviewPage/ReviewPage.spec.tsx +++ b/website/src/components/ReviewPage/ReviewPage.spec.tsx @@ -3,7 +3,14 @@ import userEvent from '@testing-library/user-event'; import { describe, expect, test } from 'vitest'; import { ReviewPage } from './ReviewPage.tsx'; -import { mockRequest, testAccessToken, testConfig, testGroups, testOrganism } from '../../../vitest.setup.ts'; +import { + defaultReviewData, + mockRequest, + testAccessToken, + testConfig, + testGroups, + testOrganism, +} from '../../../vitest.setup.ts'; import { approvedForReleaseStatus, processedStatus, @@ -86,6 +93,18 @@ const awaitingApprovalTestData: SequenceEntryStatus = { submitter: 'submitter', }; +const revocationTestData: SequenceEntryStatus = { + submissionId: 'custom5', + status: processedStatus, + processingResult: noIssuesProcessingResult, + accession: 'accession5', + version: 2, + isRevocation: true, + dataUseTerms: openDataUseTerms, + groupId: 42, + submitter: 'submitter', +}; + const emptyStatusCounts = { [receivedStatus]: 0, [inProcessingStatus]: 0, @@ -166,7 +185,7 @@ describe('ReviewPage', () => { 200, generateGetSequencesResponse([erroneousTestData, awaitingApprovalTestData]), ); - mockRequest.backend.getDataToEdit(); + mockRequest.backend.getOriginalDataForEntry(); mockRequest.backend.approveSequences(); mockRequest.backend.deleteSequences(); @@ -201,6 +220,31 @@ describe('ReviewPage', () => { }); }); + test('should show the version comment for revocation entries', async () => { + mockRequest.backend.getSequences(200, generateGetSequencesResponse([revocationTestData])); + mockRequest.backend.getOriginalDataForEntry(200, { + ...defaultReviewData, + accession: revocationTestData.accession, + version: revocationTestData.version, + isRevocation: true, + errors: null, + warnings: null, + originalData: { + metadata: { versionComment: 'Withdrawn due to contamination' }, + unalignedNucleotideSequences: {}, + files: null, + }, + processedData: null, + }); + + const { getByText } = renderReviewPage(); + + await waitFor(() => { + expect(getByText('Version comment')).toBeDefined(); + expect(getByText('Withdrawn due to contamination')).toBeDefined(); + }); + }); + test('should render the review page and show how many sequences are processed', async () => { mockRequest.backend.getSequences( 200, @@ -211,7 +255,7 @@ describe('ReviewPage', () => { awaitingApprovalTestData, ]), ); - mockRequest.backend.getDataToEdit(); + mockRequest.backend.getOriginalDataForEntry(); const { getByText } = renderReviewPage(); diff --git a/website/src/components/ReviewPage/SequencesDialog.spec.tsx b/website/src/components/ReviewPage/SequencesDialog.spec.tsx index 126aedfed6..41684bd75f 100644 --- a/website/src/components/ReviewPage/SequencesDialog.spec.tsx +++ b/website/src/components/ReviewPage/SequencesDialog.spec.tsx @@ -70,6 +70,7 @@ const dataToView = (sequence1: string, sequence2: string, gene1: string, gene2: accession: 'test', version: 0, groupId: 0, + isRevocation: false, originalData: { metadata: {}, unalignedNucleotideSequences: {}, diff --git a/website/src/components/ReviewPage/SequencesDialog.tsx b/website/src/components/ReviewPage/SequencesDialog.tsx index 81d8a56fa8..77ac01083b 100644 --- a/website/src/components/ReviewPage/SequencesDialog.tsx +++ b/website/src/components/ReviewPage/SequencesDialog.tsx @@ -22,7 +22,7 @@ type ProcessedSequence = { export const SequencesDialog: FC = ({ isOpen, onClose, dataToView, referenceGenomesInfo }) => { const [activeTab, setActiveTab] = useState(0); - if (!isOpen || !dataToView) return null; + if (!isOpen || !dataToView?.processedData) return null; const processedSequences = extractProcessedSequences(dataToView, lapisNameToDisplayName(referenceGenomesInfo)); @@ -66,10 +66,12 @@ const extractProcessedSequences = ( data: SequenceEntryToEdit, lapisNameToDisplayNameMap: Map, ): ProcessedSequence[] => { + if (data.processedData === null) return []; + const processedData = data.processedData; return [ - { type: 'unaligned', sequences: data.processedData.unalignedNucleotideSequences }, - { type: 'aligned', sequences: data.processedData.alignedNucleotideSequences }, - { type: 'gene', sequences: data.processedData.alignedAminoAcidSequences }, + { type: 'unaligned', sequences: processedData.unalignedNucleotideSequences }, + { type: 'aligned', sequences: processedData.alignedNucleotideSequences }, + { type: 'gene', sequences: processedData.alignedAminoAcidSequences }, ].flatMap(({ type, sequences }) => Object.entries(sequences) .filter((tuple): tuple is [string, string] => tuple[1] !== null) diff --git a/website/src/pages/[organism]/submission/edit/[accession]/[version].astro b/website/src/pages/[organism]/submission/edit/[accession]/[version].astro index b295473341..f240e0ab68 100644 --- a/website/src/pages/[organism]/submission/edit/[accession]/[version].astro +++ b/website/src/pages/[organism]/submission/edit/[accession]/[version].astro @@ -25,7 +25,7 @@ const accessToken = getAccessToken(Astro.locals.session)!; const clientConfig = getRuntimeConfig().public; const schema = getSchema(organism); -const dataToEdit = await createBackendClient().getDataToEdit(organism, accessToken, accession, version); +const dataToEdit = await createBackendClient().getOriginalDataForEntry(organism, accessToken, accession, version); --- diff --git a/website/src/services/backendApi.ts b/website/src/services/backendApi.ts index 7ddb8d9eb8..cb9c136265 100644 --- a/website/src/services/backendApi.ts +++ b/website/src/services/backendApi.ts @@ -69,10 +69,10 @@ const reviseEndpoint = makeEndpoint({ ], }); -const getDataToEditEndpoint = makeEndpoint({ +const getOriginalDataForEntryEndpoint = makeEndpoint({ method: 'get', - path: withOrganismPathSegment('/get-data-to-edit/:accession/:version'), - alias: 'getDataToEdit', + path: withOrganismPathSegment('/get-original-data-for-entry/:accession/:version'), + alias: 'getOriginalDataForEntry', parameters: [authorizationHeader], response: sequenceEntryToEdit, errors: [notAuthorizedError], @@ -256,7 +256,7 @@ const setDataUseTerms = makeEndpoint({ export const backendApi = makeApi([ submitEndpoint, reviseEndpoint, - getDataToEditEndpoint, + getOriginalDataForEntryEndpoint, revokeSequencesEndpoint, submitReviewedSequenceEndpoint, getSequencesEndpoint, diff --git a/website/src/services/backendClient.ts b/website/src/services/backendClient.ts index ac9d3d1cb1..da97899c41 100644 --- a/website/src/services/backendClient.ts +++ b/website/src/services/backendClient.ts @@ -25,9 +25,9 @@ type GetSequencesParameters = { export class BackendClient { constructor(private readonly url: string) {} - public getDataToEdit(organism: string, token: string, accession: string, version: string | number) { + public getOriginalDataForEntry(organism: string, token: string, accession: string, version: string | number) { return this.request( - `/${organism}/get-data-to-edit/${accession}/${version}`, + `/${organism}/get-original-data-for-entry/${accession}/${version}`, 'GET', sequenceEntryToEdit, createAuthorizationHeader(token), diff --git a/website/src/types/backend.ts b/website/src/types/backend.ts index db984b39cb..59570090b2 100644 --- a/website/src/types/backend.ts +++ b/website/src/types/backend.ts @@ -220,6 +220,7 @@ export const sequenceEntryToEdit = accessionVersion.merge( status: sequenceEntryStatusNames, groupId: z.number(), submissionId: z.string(), + isRevocation: z.boolean(), errors: z.array(processingAnnotation).nullable(), warnings: z.array(processingAnnotation).nullable(), originalData: z.object({ @@ -227,16 +228,18 @@ export const sequenceEntryToEdit = accessionVersion.merge( unalignedNucleotideSequences: z.record(z.string()), files: filesByCategory.nullable(), }), - processedData: z.object({ - metadata: metadataRecord, - unalignedNucleotideSequences: z.record(z.string().nullable()), - alignedNucleotideSequences: z.record(z.string().nullable()), - nucleotideInsertions: z.record(z.array(z.string())), - alignedAminoAcidSequences: z.record(z.string().nullable()), - aminoAcidInsertions: z.record(z.array(z.string())), - sequenceNameToFastaId: z.record(z.string().nullable()), - files: filesByCategory.nullable(), - }), + processedData: z + .object({ + metadata: metadataRecord, + unalignedNucleotideSequences: z.record(z.string().nullable()), + alignedNucleotideSequences: z.record(z.string().nullable()), + nucleotideInsertions: z.record(z.array(z.string())), + alignedAminoAcidSequences: z.record(z.string().nullable()), + aminoAcidInsertions: z.record(z.array(z.string())), + sequenceNameToFastaId: z.record(z.string().nullable()), + files: filesByCategory.nullable(), + }) + .nullable(), }), ); export type SequenceEntryToEdit = z.infer; diff --git a/website/vitest.setup.ts b/website/vitest.setup.ts index b705157c91..c9ecd8fc7d 100755 --- a/website/vitest.setup.ts +++ b/website/vitest.setup.ts @@ -62,6 +62,7 @@ export const defaultReviewData: SequenceEntryToEdit = { version: 1, status: 'PROCESSED', groupId: 1, + isRevocation: false, errors: [ { unprocessedFields: [ @@ -173,9 +174,9 @@ const backendRequestMocks = { }), ); }, - getDataToEdit: (statusCode: number = 200, response = defaultReviewData) => { + getOriginalDataForEntry: (statusCode: number = 200, response = defaultReviewData) => { testServer.use( - http.get(`${testConfig.serverSide.backendUrl}/${testOrganism}/get-data-to-edit/*`, () => { + http.get(`${testConfig.serverSide.backendUrl}/${testOrganism}/get-original-data-for-entry/*`, () => { return new Response(JSON.stringify(response), { status: statusCode, });