diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/config/DashboardsConfig.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/config/DashboardsConfig.kt index 1ea0a5bb..ac3e937d 100644 --- a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/config/DashboardsConfig.kt +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/config/DashboardsConfig.kt @@ -13,9 +13,19 @@ data class DashboardsConfig(val organisms: Map) { throw BadRequestException("Organism '$organism' is not supported") } } + + fun validateCollectionsEnabled(organism: String) { + if (!getOrganismConfig(organism).hasCollections) { + throw BadRequestException("Collections are not supported for organism '$organism'") + } + } } -data class OrganismConfig(val lapis: LapisConfig, val externalNavigationLinks: List?) +data class OrganismConfig( + val lapis: LapisConfig, + val externalNavigationLinks: List?, + val hasCollections: Boolean = true, +) data class LapisConfig( val url: String, diff --git a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionModel.kt b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionModel.kt index 225d504c..5679b527 100644 --- a/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionModel.kt +++ b/backend/src/main/kotlin/org/genspectrum/dashboardsbackend/model/collection/CollectionModel.kt @@ -22,6 +22,10 @@ import org.springframework.transaction.annotation.Transactional @Transactional class CollectionModel(private val dashboardsConfig: DashboardsConfig) { fun getCollections(userId: String?, organism: String?): List { + if (organism != null) { + dashboardsConfig.validateIsValidOrganism(organism) + dashboardsConfig.validateCollectionsEnabled(organism) + } val query = if (userId == null && organism == null) { CollectionEntity.all() } else { @@ -60,12 +64,16 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) { } } - fun getCollection(id: Long): Collection = CollectionEntity.findById(id) - ?.toCollection() - ?: throw NotFoundException("Collection $id not found") + fun getCollection(id: Long): Collection { + val entity = CollectionEntity.findById(id) + ?: throw NotFoundException("Collection $id not found") + dashboardsConfig.validateCollectionsEnabled(entity.organism) + return entity.toCollection() + } fun createCollection(request: CollectionRequest, userId: String): Collection { dashboardsConfig.validateIsValidOrganism(request.organism) + dashboardsConfig.validateCollectionsEnabled(request.organism) val collectionEntity = CollectionEntity.new { name = request.name @@ -94,6 +102,7 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) { // Find with ownership check val entity = CollectionEntity.findForUser(id, userId) ?: throw ForbiddenException("Collection $id not found or you don't have permission to delete it") + dashboardsConfig.validateCollectionsEnabled(entity.organism) // Delete (variants cascade automatically via DB constraint) entity.delete() @@ -102,6 +111,7 @@ class CollectionModel(private val dashboardsConfig: DashboardsConfig) { fun putCollection(id: Long, update: CollectionUpdate, userId: String): Collection { val collectionEntity = CollectionEntity.findForUser(id, userId) ?: throw ForbiddenException("Collection $id not found or you don't have permission to update it") + dashboardsConfig.validateCollectionsEnabled(collectionEntity.organism) if (update.name != null) { collectionEntity.name = update.name diff --git a/backend/src/main/resources/application-dashboards-prod.yaml b/backend/src/main/resources/application-dashboards-prod.yaml index f170d623..c5410068 100644 --- a/backend/src/main/resources/application-dashboards-prod.yaml +++ b/backend/src/main/resources/application-dashboards-prod.yaml @@ -49,6 +49,7 @@ dashboards: menuIcon: "database" description: "Browse the underlying data in the Loculus database." influenzaA: + hasCollections: false lapis: url: "https://api.loculus.genspectrum.org/influenza-a" mainDateField: "sampleCollectionDateRangeLower" @@ -61,6 +62,7 @@ dashboards: menuIcon: "database" description: "Browse the underlying data in the Loculus database." influenzaB: + hasCollections: false lapis: url: "https://api.loculus.genspectrum.org/influenza-b" mainDateField: "sampleCollectionDateRangeLower" diff --git a/backend/src/main/resources/application-dashboards-staging.yaml b/backend/src/main/resources/application-dashboards-staging.yaml index d383a1e4..7b5abd22 100644 --- a/backend/src/main/resources/application-dashboards-staging.yaml +++ b/backend/src/main/resources/application-dashboards-staging.yaml @@ -46,6 +46,7 @@ dashboards: menuIcon: "database" description: "Browse the underlying data in the Loculus database." influenzaA: + hasCollections: false lapis: url: "https://api.loculus.staging.genspectrum.org/influenza-a" mainDateField: "sampleCollectionDateRangeLower" @@ -58,6 +59,7 @@ dashboards: menuIcon: "database" description: "Browse the underlying data in the Loculus database." influenzaB: + hasCollections: false lapis: url: "https://api.loculus.staging.genspectrum.org/influenza-b" mainDateField: "sampleCollectionDateRangeLower" diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/TestHelpers.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/TestHelpers.kt index fbfde12c..e23670b6 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/TestHelpers.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/TestHelpers.kt @@ -13,6 +13,8 @@ enum class KnownTestOrganisms { WestNile, } +const val ORGANISM_WITHOUT_COLLECTIONS = "InfluenzaA" + val dummySubscriptionRequest = SubscriptionRequest( name = "My search", interval = EvaluationInterval.MONTHLY, diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsGetTest.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsGetTest.kt index 3f6102a3..d813ea6b 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsGetTest.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsGetTest.kt @@ -1,6 +1,7 @@ package org.genspectrum.dashboardsbackend.controller import org.genspectrum.dashboardsbackend.KnownTestOrganisms +import org.genspectrum.dashboardsbackend.ORGANISM_WITHOUT_COLLECTIONS import org.genspectrum.dashboardsbackend.api.Variant import org.genspectrum.dashboardsbackend.dummyCollectionRequest import org.hamcrest.MatcherAssert.assertThat @@ -266,4 +267,11 @@ class CollectionsGetTest( mockMvc.perform(get("/collections/not-a-number")) .andExpect(status().isBadRequest) } + + @Test + fun `WHEN getting collections for organism with collections disabled THEN returns 400`() { + collectionsClient.getCollectionsRaw(organism = ORGANISM_WITHOUT_COLLECTIONS) + .andExpect(status().isBadRequest) + .andExpect(jsonPath("$.detail").value("Collections are not supported for organism 'InfluenzaA'")) + } } diff --git a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPostTest.kt b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPostTest.kt index b318b05f..9aa72fe9 100644 --- a/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPostTest.kt +++ b/backend/src/test/kotlin/org/genspectrum/dashboardsbackend/controller/CollectionsPostTest.kt @@ -1,5 +1,6 @@ package org.genspectrum.dashboardsbackend.controller +import org.genspectrum.dashboardsbackend.ORGANISM_WITHOUT_COLLECTIONS import org.genspectrum.dashboardsbackend.api.FilterObject import org.genspectrum.dashboardsbackend.api.Variant import org.genspectrum.dashboardsbackend.api.VariantRequest @@ -92,6 +93,17 @@ class CollectionsPostTest( .andExpect(jsonPath("\$.detail").value("Organism 'unknown organism' is not supported")) } + @Test + fun `WHEN creating collection for organism with collections disabled THEN returns 400`() { + val userId = getNewUserId() + collectionsClient.postCollectionRaw( + dummyCollectionRequest.copy(organism = ORGANISM_WITHOUT_COLLECTIONS), + userId, + ) + .andExpect(status().isBadRequest) + .andExpect(jsonPath("\$.detail").value("Collections are not supported for organism 'InfluenzaA'")) + } + @Test fun `WHEN creating variant with lineage filter THEN succeeds`() { val userId = getNewUserId() diff --git a/backend/src/test/resources/application.yaml b/backend/src/test/resources/application.yaml index afafa819..f3728e69 100644 --- a/backend/src/test/resources/application.yaml +++ b/backend/src/test/resources/application.yaml @@ -28,3 +28,8 @@ dashboards: mainDateField: "westnile_date" additionalFilters: someAdditionalFilter: "westnile_additional_filter" + InfluenzaA: + hasCollections: false + lapis: + url: "https://influenzaA.lapis.dummy" + mainDateField: "influenzaA_date" diff --git a/website/routeMocker.ts b/website/routeMocker.ts index 0c4e7adc..98e43c84 100644 --- a/website/routeMocker.ts +++ b/website/routeMocker.ts @@ -272,12 +272,14 @@ function getError(error: unknown) { export const testOrganismsConfig = { covid: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'date', }, }, influenzaA: { + hasCollections: false, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -288,6 +290,7 @@ export const testOrganismsConfig = { }, }, h3n2: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -298,6 +301,7 @@ export const testOrganismsConfig = { }, }, h1n1pdm: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -308,6 +312,7 @@ export const testOrganismsConfig = { }, }, h5n1: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -318,6 +323,7 @@ export const testOrganismsConfig = { }, }, influenzaB: { + hasCollections: false, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -328,6 +334,7 @@ export const testOrganismsConfig = { }, }, victoria: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -338,6 +345,7 @@ export const testOrganismsConfig = { }, }, westNile: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -348,6 +356,7 @@ export const testOrganismsConfig = { }, }, rsvA: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -358,6 +367,7 @@ export const testOrganismsConfig = { }, }, rsvB: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -368,6 +378,7 @@ export const testOrganismsConfig = { }, }, mpox: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -378,6 +389,7 @@ export const testOrganismsConfig = { }, }, ebolaSudan: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -388,6 +400,7 @@ export const testOrganismsConfig = { }, }, ebolaZaire: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -398,6 +411,7 @@ export const testOrganismsConfig = { }, }, cchf: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -408,6 +422,7 @@ export const testOrganismsConfig = { }, }, denv1: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -418,6 +433,7 @@ export const testOrganismsConfig = { }, }, denv2: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -428,6 +444,7 @@ export const testOrganismsConfig = { }, }, denv3: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -438,6 +455,7 @@ export const testOrganismsConfig = { }, }, denv4: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', @@ -448,6 +466,7 @@ export const testOrganismsConfig = { }, }, measles: { + hasCollections: true, lapis: { url: DUMMY_LAPIS_URL, mainDateField: 'sampleCollectionDateRangeLower', diff --git a/website/src/config.ts b/website/src/config.ts index c6f05418..28b38801 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -25,6 +25,7 @@ const externalNavigationLinksSchema = z.optional(z.array(externalNavigationLinkS const organismConfigSchema = z.object({ lapis: lapisConfigSchema, externalNavigationLinks: externalNavigationLinksSchema, + hasCollections: z.boolean().default(true), }); export type OrganismConfig = z.infer; @@ -120,7 +121,7 @@ function processEnvOrMetaEnv(key: string, schema: z.ZodType) { ); } -function readTypedConfigFile(fileName: string, schema: z.ZodType) { +function readTypedConfigFile(fileName: string, schema: z.ZodType) { const configFilePath = path.join(getConfigDir(), fileName); const yaml = YAML.parse(fs.readFileSync(configFilePath, 'utf8')); try { diff --git a/website/src/layouts/base/header/getPathogenMegaMenuSections.ts b/website/src/layouts/base/header/getPathogenMegaMenuSections.ts index 5839ee90..f67259ba 100644 --- a/website/src/layouts/base/header/getPathogenMegaMenuSections.ts +++ b/website/src/layouts/base/header/getPathogenMegaMenuSections.ts @@ -1,5 +1,5 @@ import type { MenuIconType } from '../../../components/iconCss.ts'; -import { isStaging } from '../../../config.ts'; +import { getOrganismConfig, isStaging } from '../../../config.ts'; import { type Organism, organismConfig, paths } from '../../../types/Organism.ts'; import { Page } from '../../../types/pages.ts'; import { @@ -49,20 +49,21 @@ export function getPathogenMegaMenuSections(): PathogenMegaMenuSections { }; }); + const supplementaryEntries: MegaMenuSection[] = []; + // only on staging for now, remove when enabling on prod: https://github.com/GenSpectrum/dashboards/issues/1108 - if (isStaging()) { - megaMenuSections.push({ + if (isStaging() && getOrganismConfig(config.organism).hasCollections) { + supplementaryEntries.push({ label: 'Collections', href: Page.collectionsForOrganism(config.organism), underlineColor: config.menuListEntryDecoration, iconType: 'table', externalLink: false, description: `Browse ${config.label} variant collections`, - hasSeparatorAbove: true, }); } - megaMenuSections.push( + supplementaryEntries.push( ...ServerSide.routing.externalPages[config.organism].map((externalPage) => ({ label: externalPage.label, href: externalPage.url, @@ -73,6 +74,12 @@ export function getPathogenMegaMenuSections(): PathogenMegaMenuSections { })), ); + if (supplementaryEntries.length > 0) { + supplementaryEntries[0] = { ...supplementaryEntries[0], hasSeparatorAbove: true }; + } + + megaMenuSections.push(...supplementaryEntries); + acc[config.organism] = { headline: config.label, headlineBackgroundColor: config.backgroundColor, diff --git a/website/src/pages/collections/index.astro b/website/src/pages/collections/index.astro index 12f95b1c..031dfbab 100644 --- a/website/src/pages/collections/index.astro +++ b/website/src/pages/collections/index.astro @@ -1,5 +1,5 @@ --- -import { isStaging } from '../../config'; +import { getOrganismConfig, isStaging } from '../../config'; import { defaultBreadcrumbs } from '../../layouts/Breadcrumbs'; import ContaineredPageLayout from '../../layouts/ContaineredPage/ContaineredPageLayout.astro'; import { allOrganisms, organismConfig } from '../../types/Organism'; @@ -19,23 +19,25 @@ if (!isStaging()) {

Select an organism to browse variant collections.