mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Fix locality project link persistence
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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 },
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user