Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ data class DashboardsConfig(val organisms: Map<String, OrganismConfig>) {
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<ExternalNavigationLink>?)
data class OrganismConfig(
val lapis: LapisConfig,
val externalNavigationLinks: List<ExternalNavigationLink>?,
val hasCollections: Boolean = true,
)

data class LapisConfig(
val url: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import org.springframework.transaction.annotation.Transactional
@Transactional
class CollectionModel(private val dashboardsConfig: DashboardsConfig) {
fun getCollections(userId: String?, organism: String?): List<Collection> {
if (organism != null) {
dashboardsConfig.validateIsValidOrganism(organism)
dashboardsConfig.validateCollectionsEnabled(organism)
}
val query = if (userId == null && organism == null) {
CollectionEntity.all()
} else {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main/resources/application-dashboards-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ enum class KnownTestOrganisms {
WestNile,
}

const val ORGANISM_WITHOUT_COLLECTIONS = "InfluenzaA"

val dummySubscriptionRequest = SubscriptionRequest(
name = "My search",
interval = EvaluationInterval.MONTHLY,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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'"))
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions backend/src/test/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
19 changes: 19 additions & 0 deletions website/routeMocker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -288,6 +290,7 @@ export const testOrganismsConfig = {
},
},
h3n2: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -298,6 +301,7 @@ export const testOrganismsConfig = {
},
},
h1n1pdm: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -308,6 +312,7 @@ export const testOrganismsConfig = {
},
},
h5n1: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -318,6 +323,7 @@ export const testOrganismsConfig = {
},
},
influenzaB: {
hasCollections: false,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -328,6 +334,7 @@ export const testOrganismsConfig = {
},
},
victoria: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -338,6 +345,7 @@ export const testOrganismsConfig = {
},
},
westNile: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -348,6 +356,7 @@ export const testOrganismsConfig = {
},
},
rsvA: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -358,6 +367,7 @@ export const testOrganismsConfig = {
},
},
rsvB: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -368,6 +378,7 @@ export const testOrganismsConfig = {
},
},
mpox: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -378,6 +389,7 @@ export const testOrganismsConfig = {
},
},
ebolaSudan: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -388,6 +400,7 @@ export const testOrganismsConfig = {
},
},
ebolaZaire: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -398,6 +411,7 @@ export const testOrganismsConfig = {
},
},
cchf: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -408,6 +422,7 @@ export const testOrganismsConfig = {
},
},
denv1: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -418,6 +433,7 @@ export const testOrganismsConfig = {
},
},
denv2: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -428,6 +444,7 @@ export const testOrganismsConfig = {
},
},
denv3: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -438,6 +455,7 @@ export const testOrganismsConfig = {
},
},
denv4: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand All @@ -448,6 +466,7 @@ export const testOrganismsConfig = {
},
},
measles: {
hasCollections: true,
lapis: {
url: DUMMY_LAPIS_URL,
mainDateField: 'sampleCollectionDateRangeLower',
Expand Down
3 changes: 2 additions & 1 deletion website/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof organismConfigSchema>;

Expand Down Expand Up @@ -120,7 +121,7 @@ function processEnvOrMetaEnv<T>(key: string, schema: z.ZodType<T>) {
);
}

function readTypedConfigFile<T>(fileName: string, schema: z.ZodType<T>) {
function readTypedConfigFile<T>(fileName: string, schema: z.ZodType<T, z.ZodTypeDef, unknown>) {
const configFilePath = path.join(getConfigDir(), fileName);
const yaml = YAML.parse(fs.readFileSync(configFilePath, 'utf8'));
try {
Expand Down
17 changes: 12 additions & 5 deletions website/src/layouts/base/header/getPathogenMegaMenuSections.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading