Fix locality project link persistence

This commit is contained in:
Kari Lintulaakso
2025-11-19 13:24:26 +02:00
parent c0c329105f
commit 52f121c014
5 changed files with 130 additions and 1 deletions
@@ -0,0 +1,68 @@
import { afterAll, beforeEach, describe, expect, it } from '@jest/globals'
import { LocalityDetailsType } from '../../../../frontend/src/shared/types'
import { editedLocality } from './data'
import { login, resetDatabase, resetDatabaseTimeout, send, noPermError } from '../utils'
import { pool } from '../../utils/db'
const TEST_PROJECT_ID = 35
const localityId = editedLocality.lid
const cloneEditedLocality = () => structuredClone(editedLocality)
const buildLocalityPayload = (projectIds: number[]) => {
const payload = cloneEditedLocality()
payload.now_plr = projectIds.map(pid => ({
lid: payload.lid,
pid,
rowState: 'new' as const,
}))
return payload
}
describe('Locality project links', () => {
beforeEach(async () => {
await resetDatabase()
}, resetDatabaseTimeout)
afterAll(async () => {
await pool.end()
})
it('adds a project to a locality and exposes it via the API', async () => {
await login()
const payload = buildLocalityPayload([TEST_PROJECT_ID])
const result = await send<{ id: number }>('locality', 'PUT', { locality: payload })
expect(result.status).toEqual(200)
expect(result.body.id).toEqual(localityId)
const localityResponse = await send<LocalityDetailsType>(`locality/${localityId}`, 'GET')
expect(localityResponse.status).toEqual(200)
expect(localityResponse.body.now_plr.some(link => link.pid === TEST_PROJECT_ID)).toEqual(true)
})
it('rejects users without project edit permissions', async () => {
await login('testEr')
const payload = buildLocalityPayload([TEST_PROJECT_ID])
const result = await send('locality', 'PUT', { locality: payload })
expect(result.status).toEqual(403)
expect(result.body).toEqual(noPermError)
})
it('ignores duplicate additions of the same project', async () => {
await login()
const duplicatePayload = buildLocalityPayload([TEST_PROJECT_ID, TEST_PROJECT_ID])
const firstResult = await send<{ id: number }>('locality', 'PUT', { locality: duplicatePayload })
expect(firstResult.status).toEqual(200)
const duplicateAttempt = buildLocalityPayload([TEST_PROJECT_ID])
const secondResult = await send<{ id: number }>('locality', 'PUT', { locality: duplicateAttempt })
expect(secondResult.status).toEqual(200)
const localityResponse = await send<LocalityDetailsType>(`locality/${localityId}`, 'GET')
const matchingLinks = localityResponse.body.now_plr.filter(link => link.pid === TEST_PROJECT_ID)
expect(matchingLinks).toHaveLength(1)
})
})
+15
View File
@@ -7,7 +7,9 @@ import {
LocalitySpeciesDetailsType,
Editable,
SpeciesDetailsType,
LocalityProject,
} from '../../../frontend/src/shared/types'
import { removeDuplicateProjectLinks } from './utils/projectLinks'
import { validateLocality } from '../../../frontend/src/shared/validators/locality'
import { validateSpecies } from '../../../frontend/src/shared/validators/species'
import { ValidationObject, referenceValidator } from '../../../frontend/src/shared/validators/validator'
@@ -306,3 +308,16 @@ export const filterDuplicateLocalitySpecies = async (
return updatedLocalityNow_ls
}
export const filterDuplicateLocalityProjects = async (
locality: EditDataType<LocalityDetailsType>,
user: User | undefined
) => {
if (!locality.lid || !locality.now_plr?.length) return locality.now_plr
const localityDetails = await getLocalityDetails(locality.lid, user)
if (!localityDetails) return locality.now_plr
const existingProjectIds = new Set(localityDetails.now_plr.map(project => project.pid))
return removeDuplicateProjectLinks(locality.now_plr, existingProjectIds)
}
@@ -0,0 +1,16 @@
import { Editable, LocalityProject } from '../../../../frontend/src/shared/types'
export const removeDuplicateProjectLinks = <
T extends { pid: number; rowState?: Editable<LocalityProject>['rowState'] }
>(pendingLinks: T[], existingProjectIds: Set<number>) => {
const seenNew = new Set<number>()
return pendingLinks.filter(link => {
if (link.rowState === 'new') {
if (existingProjectIds.has(link.pid) || seenNew.has(link.pid)) {
return false
}
seenNew.add(link.pid)
}
return true
})
}
+8 -1
View File
@@ -3,7 +3,7 @@ import { getHomininSkeletalRemains } from '../../../../frontend/src/shared/calcu
import { NOW_DB_NAME } from '../../utils/config'
import { WriteHandler } from './writeOperations/writeHandler'
import { getFieldsOfTables } from '../../utils/db'
import { getLocalityDetails, filterDuplicateLocalitySpecies } from '../locality'
import { getLocalityDetails, filterDuplicateLocalitySpecies, filterDuplicateLocalityProjects } from '../locality'
import { ActionType } from './writeOperations/types'
import { makeListRemoved, fixRadioSelection } from './writeOperations/utils'
@@ -43,6 +43,7 @@ export const writeLocality = async (
locality.stone_tool_cut_marks_on_bones = fixRadioSelection(locality.stone_tool_cut_marks_on_bones) as boolean
locality.bipedal_footprints = fixRadioSelection(locality.bipedal_footprints) as boolean
locality.stone_tool_technology = fixRadioSelection(locality.stone_tool_technology) as boolean
locality.now_plr = locality.now_plr ?? []
const authorizer = user!.initials
@@ -54,6 +55,11 @@ export const writeLocality = async (
locality.now_ls = filteredLocalities
}
const filteredProjects = await filterDuplicateLocalityProjects(locality, user)
if (filteredProjects) {
locality.now_plr = filteredProjects
}
if (updateOrAdd === 'add') {
const { lid: newLid } = await writeHandler.createObject('now_loc', locality, ['lid'])
locality.lid = newLid as number
@@ -77,6 +83,7 @@ export const writeLocality = async (
await writeHandler.applyListChanges('now_ss', locality.now_ss, ['lid', 'sed_struct'])
await writeHandler.applyListChanges('now_coll_meth', locality.now_coll_meth, ['lid', 'coll_meth'])
await writeHandler.applyListChanges('now_syn_loc', locality.now_syn_loc, ['lid', 'syn_id'])
await writeHandler.applyListChanges('now_plr', locality.now_plr, ['lid', 'pid'])
await writeHandler.logUpdatesAndComplete(authorizer, comment ?? '', references ?? [])
return locality.lid
@@ -0,0 +1,23 @@
import { describe, expect, it } from '@jest/globals'
import { removeDuplicateProjectLinks } from '../../services/utils/projectLinks'
describe('removeDuplicateProjectLinks', () => {
it('filters out duplicates while preserving removals', () => {
const existingProjectIds = new Set([35])
const pendingLinks = [
{ lid: 1, pid: 35, rowState: 'new' as const },
{ lid: 1, pid: 36, rowState: 'new' as const },
{ lid: 1, pid: 36, rowState: 'new' as const },
{ lid: 1, pid: 36, rowState: 'removed' as const },
{ lid: 1, pid: 37 },
]
const result = removeDuplicateProjectLinks(pendingLinks, existingProjectIds)
expect(result).toEqual([
{ lid: 1, pid: 36, rowState: 'new' },
{ lid: 1, pid: 36, rowState: 'removed' },
{ lid: 1, pid: 37 },
])
})
})