Merge branch 'main' into improve-export-filenames

This commit is contained in:
Kari Lintulaakso
2025-10-09 17:01:17 +03:00
committed by GitHub
30 changed files with 785 additions and 394 deletions
+3
View File
@@ -40,3 +40,6 @@ The new version is not deployed yet.
:mag_right: [Class diagram](documentation/class_diagram.md) See the relations of relevant tables. Notice that it omits some unimportant tables and does not show most columns. :mag_right: [Class diagram](documentation/class_diagram.md) See the relations of relevant tables. Notice that it omits some unimportant tables and does not show most columns.
:earth_africa: [Map features](documentation/frontend/components/maps/README.md) Maps' functionality and development notes :earth_africa: [Map features](documentation/frontend/components/maps/README.md) Maps' functionality and development notes
**DeepWiki documentation for the NOW database can be found here:**
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/nowcommunity/nowdatabase)
+3 -3
View File
@@ -43,9 +43,9 @@ export const newSpeciesBasis: EditDataType<SpeciesDetailsType & EditMetaData> =
suborder_or_superfamily_name: '', suborder_or_superfamily_name: '',
subfamily_name: 'Soricinae', subfamily_name: 'Soricinae',
genus_name: 'Petenyia', genus_name: 'Petenyia',
species_name: 'dubia test', species_name: 'dubiatest',
unique_identifier: '-', unique_identifier: '-',
taxonomic_status: null, taxonomic_status: '',
common_name: null, common_name: null,
sp_author: 'Bachmayer & Wilson, 1970', sp_author: 'Bachmayer & Wilson, 1970',
strain: null, strain: null,
@@ -145,7 +145,7 @@ export const newSpeciesBasis: EditDataType<SpeciesDetailsType & EditMetaData> =
export const editedSpecies: EditDataType<SpeciesDetailsType & EditMetaData> = { export const editedSpecies: EditDataType<SpeciesDetailsType & EditMetaData> = {
species_id: 21426, species_id: 21426,
sp_comment: 'Test comment', sp_comment: 'Test comment',
species_name: 'Test species name', species_name: 'Testspeciesname',
now_ls: [{ rowState: 'new', lid: 20920 }], now_ls: [{ rowState: 'new', lid: 20920 }],
com_taxa_synonym: [], com_taxa_synonym: [],
now_sau: [], now_sau: [],
+27 -8
View File
@@ -1,11 +1,20 @@
import { EditDataType, EditMetaData, LocalityDetailsType, User, Role } from '../../../frontend/src/shared/types' import {
EditDataType,
EditMetaData,
LocalityDetailsType,
User,
Role,
LocalitySpeciesDetailsType,
Editable,
SpeciesDetailsType,
} from '../../../frontend/src/shared/types'
import { validateLocality } from '../../../frontend/src/shared/validators/locality' 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' import { ValidationObject, referenceValidator } from '../../../frontend/src/shared/validators/validator'
import Prisma from '../../prisma/generated/now_test_client' import Prisma from '../../prisma/generated/now_test_client'
import { AccessError } from '../middlewares/authorizer' import { AccessError } from '../middlewares/authorizer'
import { fixBigInt } from '../utils/common' import { fixBigInt } from '../utils/common'
import { logDb, nowDb } from '../utils/db' import { logDb, nowDb } from '../utils/db'
import { logger } from '../utils/logger'
import { getReferenceDetails } from './reference' import { getReferenceDetails } from './reference'
const getIdsOfUsersProjects = async (user: User) => { const getIdsOfUsersProjects = async (user: User) => {
@@ -200,6 +209,7 @@ export const getLocalityDetails = async (id: number, user: User | undefined) =>
return JSON.parse(fixBigInt(result)!) as LocalityDetailsType return JSON.parse(fixBigInt(result)!) as LocalityDetailsType
} }
// also validates possible new species that were added to this locality
export const validateEntireLocality = async (editedFields: EditDataType<Prisma.now_loc> & EditMetaData) => { export const validateEntireLocality = async (editedFields: EditDataType<Prisma.now_loc> & EditMetaData) => {
const keys = Object.keys(editedFields) const keys = Object.keys(editedFields)
const errors: ValidationObject[] = [] const errors: ValidationObject[] = []
@@ -207,6 +217,17 @@ export const validateEntireLocality = async (editedFields: EditDataType<Prisma.n
const error = validateLocality(editedFields as EditDataType<LocalityDetailsType>, key as keyof LocalityDetailsType) const error = validateLocality(editedFields as EditDataType<LocalityDetailsType>, key as keyof LocalityDetailsType)
if (error.error) errors.push(error) if (error.error) errors.push(error)
} }
if ('now_ls' in editedFields) {
for (const species of editedFields.now_ls as Array<Editable<LocalitySpeciesDetailsType>>) {
if (species.rowState === 'new') {
const keys = Object.keys(species.com_species)
for (const key of keys) {
const newSpeciesError = validateSpecies(species.com_species, key as keyof SpeciesDetailsType)
if (newSpeciesError.error) errors.push(newSpeciesError)
}
}
}
}
let error = null let error = null
if ('references' in editedFields && editedFields.references) { if ('references' in editedFields && editedFields.references) {
error = referenceValidator(editedFields.references) error = referenceValidator(editedFields.references)
@@ -251,12 +272,10 @@ export const filterDuplicateLocalitySpecies = async (
// Logic is flipped for filter as we want to skip the ones the check matches // Logic is flipped for filter as we want to skip the ones the check matches
return !(localityDetailsSpeciesIds.includes(localitySpecies.species_id) && localitySpecies.rowState === 'new') return !(localityDetailsSpeciesIds.includes(localitySpecies.species_id) && localitySpecies.rowState === 'new')
} }
// if localitySpecies doesn't have species_id (i.e. it was created with the "add new species" button in Species tab),
// code shouldn't get here ever // it will never be filtered out. However, due to the taxonomy check for the "add new species" button,
logger.info( // it should not let the user create a duplicate species in this way.
`Filter already existing species: localitySpecies didn't have species_id. Skipping. ${JSON.stringify(localitySpecies)}` return true
)
return false
}) })
return updatedLocalityNow_ls return updatedLocalityNow_ls
+3 -3
View File
@@ -49,9 +49,9 @@ export const writeLocality = async (
try { try {
await writeHandler.start() await writeHandler.start()
const filteredLocality = await filterDuplicateLocalitySpecies(locality, user) const filteredLocalities = await filterDuplicateLocalitySpecies(locality, user)
if (updateOrAdd === 'update' && filteredLocality) { if (updateOrAdd === 'update' && filteredLocalities && filteredLocalities.length > 0) {
locality.now_ls = filteredLocality locality.now_ls = filteredLocalities
} }
if (updateOrAdd === 'add') { if (updateOrAdd === 'add') {
+1 -1
View File
@@ -11,7 +11,7 @@ const getSpeciesWriteHandler = (type: ActionType) => {
dbName: NOW_DB_NAME, dbName: NOW_DB_NAME,
table: 'com_species', table: 'com_species',
idColumn: 'species_id', idColumn: 'species_id',
allowedColumns: getFieldsOfTables(['com_species', 'now_ls', 'now_sau', 'now_lau']), allowedColumns: getFieldsOfTables(['com_species', 'now_ls', 'now_sau', 'now_lau', 'com_taxa_synonym']),
type, type,
}) })
} }
+164 -20
View File
@@ -1,3 +1,162 @@
before('Reset database', () => {
cy.request(Cypress.env('databaseResetUrl'))
})
describe('Adding species in Locality -> Species tab for an existing locality', () => {
beforeEach('Login as admin', () => {
cy.login('testSu')
})
it('works with valid, unique species', () => {
cy.visit(`/locality/20920?tab=2`)
cy.contains('Lantian-Shuijiazui')
cy.get('[id=edit-button]').click()
cy.contains('Add new Species').click()
cy.get('[name=order_name]').type('Rodentia')
cy.get('[name=family_name]').type('Gliridae')
cy.get('[name=genus_name]').type('Simplomys')
cy.get('[name=species_name]').type('someSpecies')
cy.get('[name=unique_identifier]').type('veryunique')
cy.contains('Save').click()
cy.contains('somespecies')
cy.addReferenceAndSave()
cy.contains('Edited item successfully.')
cy.visit(`/locality/20920?tab=2`)
cy.contains('somespecies')
})
it('works through "copy species taxonomy" button', () => {
cy.visit(`/locality/20920?tab=2`)
cy.contains('Lantian-Shuijiazui')
cy.get('[id=edit-button]').click()
cy.contains('Add new Species').click()
cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-21426]').click()
cy.contains('Close').click()
cy.get('[name=species_name]').clear()
cy.get('[name=species_name]').type('Newspecies')
cy.contains('Save').click()
cy.addReferenceAndSave()
cy.contains('Edited item successfully.')
cy.visit(`/locality/20920?tab=2`)
cy.contains('newspecies')
})
it('does not work with invalid species', () => {
cy.visit(`/locality/20920?tab=2`)
cy.contains('Lantian-Shuijiazui')
cy.get('[id=edit-button]').click()
cy.contains('Add new Species').click()
cy.clearNewSpeciesForm()
cy.get('[name=order_name]').type('Order with spaces')
cy.get('[name=family_name]').type('newfamily')
cy.get('[name=genus_name]').type('newgenus')
cy.get('[name=species_name]').type('newspecies')
cy.get('[name=unique_identifier]').type('veryunique')
cy.contains('Save').click()
cy.contains('Order must not contain any spaces, unless the value is "incertae sedis".')
})
it('does not work if the species has already been added', () => {
cy.visit(`/locality/20920?tab=2`)
cy.contains('Lantian-Shuijiazui')
cy.get('[id=edit-button]').click()
// adding species that already exists in the database
cy.contains('Add new Species').click()
cy.clearNewSpeciesForm()
cy.get('[name=order_name]').type('Rodentia')
cy.get('[name=family_name]').type('Gliridae')
cy.get('[name=genus_name]').type('Simplomys')
cy.get('[name=species_name]').type('Simplicidens')
cy.get('[name=unique_identifier]').type('-')
cy.contains('Save').click()
cy.contains('The taxon already exists in the database.')
// add new, unique species
cy.contains('Add new Species').click()
cy.clearNewSpeciesForm()
cy.get('[name=order_name]').type('neworder')
cy.get('[name=family_name]').type('newfamily')
cy.get('[name=genus_name]').type('newgenus')
cy.get('[name=species_name]').type('newspecies')
cy.get('[name=unique_identifier]').type('-')
cy.contains('Save').click()
cy.addReferenceAndSave()
cy.contains('Edited item successfully.')
cy.visit(`/locality/20920?tab=2`)
cy.contains('Lantian-Shuijiazui')
cy.get('[id=edit-button]').click()
cy.contains('Neworder')
// adding species that has been added earlier through "add new species" button
cy.contains('Add new Species').click()
cy.clearNewSpeciesForm()
cy.get('[name=order_name]').type('neworder')
cy.get('[name=family_name]').type('newfamily')
cy.get('[name=genus_name]').type('newgenus')
cy.get('[name=species_name]').type('newspecies')
cy.get('[name=unique_identifier]').type('-')
cy.contains('Save').click()
cy.contains('The taxon already exists in the database.')
})
it('does not work with species with invalid taxonomic order', () => {
cy.visit(`/locality/20920?tab=2`)
cy.contains('Lantian-Shuijiazui')
cy.get('[id=edit-button]').click()
// adding species that already exists in the database
cy.contains('Add new Species').click()
cy.clearNewSpeciesForm()
cy.get('[name=order_name]').type('Rodentia')
cy.get('[name=family_name]').type('Soricidae')
cy.get('[name=genus_name]').type('Simplomys')
cy.get('[name=species_name]').type('Simplicidens')
cy.get('[name=unique_identifier]').type('unique')
cy.contains('Save').click()
cy.contains('Family Soricidae belongs to order Eulipotyphla, not Rodentia.')
cy.contains('Genus Simplomys belongs to family Gliridae, not Soricidae.')
cy.contains('Add new Species').click()
cy.clearNewSpeciesForm()
cy.get('[name=order_name]').type('Carnivora')
cy.get('[name=family_name]').type('Canidae')
cy.get('[name=genus_name]').type('Simplomys')
cy.get('[name=species_name]').type('Simplicidens')
cy.get('[name=unique_identifier]').type('moreunique')
cy.contains('Save').click()
cy.contains('Genus Simplomys belongs to family Gliridae, not Canidae.')
cy.contains('Add new Species').click()
cy.clearNewSpeciesForm()
cy.get('[name=order_name]').type('Artiodactyla')
cy.get('[name=family_name]').type('Bovidae')
cy.get('[name=genus_name]').type('Amblycoptus')
cy.get('[name=species_name]').type('indet.')
cy.get('[name=unique_identifier]').type('uniquest')
cy.contains('Save').click()
cy.contains('Genus Amblycoptus belongs to family Soricidae, not Bovidae.')
})
it('does not work through "copy species taxonomy" button if nothing is changed', () => {
cy.visit(`/locality/20920?tab=2`)
cy.contains('Lantian-Shuijiazui')
cy.get('[id=edit-button]').click()
cy.contains('Add new Species').click()
cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-21052]').click()
cy.contains('Close').click()
cy.contains('Save').click()
cy.contains('The taxon already exists in the database.')
})
})
describe('Creating a new locality', () => { describe('Creating a new locality', () => {
beforeEach('Login as admin', () => { beforeEach('Login as admin', () => {
cy.login('testSu') cy.login('testSu')
@@ -30,13 +189,8 @@ describe('Creating a new locality', () => {
cy.get('[id=dec_long-textfield]').type('103.67') cy.get('[id=dec_long-textfield]').type('103.67')
cy.get('[id=dms_lat-textfield]').should('have.value', '49 4 12 N') cy.get('[id=dms_lat-textfield]').should('have.value', '49 4 12 N')
cy.get('[id=dms_long-textfield]').should('have.value', '103 40 12 E') cy.get('[id=dms_long-textfield]').should('have.value', '103 40 12 E')
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled') cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('Edited item successfully.') cy.contains('Edited item successfully.')
cy.contains('Bugat') cy.contains('Bugat')
cy.get('[id=delete-button]').should('exist') cy.get('[id=delete-button]').should('exist')
@@ -150,15 +304,8 @@ describe('Creating a new locality', () => {
cy.get('[id=dec_lat-textfield]').type('49.07') cy.get('[id=dec_lat-textfield]').type('49.07')
cy.get('[id=dec_long-textfield]').type('103.67') cy.get('[id=dec_long-textfield]').type('103.67')
cy.get('[id=write-button]').should('not.be.disabled') cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled')
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.addReferenceAndSave()
cy.contains('Edited item successfully.') cy.contains('Edited item successfully.')
cy.contains('Bugat') cy.contains('Bugat')
cy.contains('8.676') cy.contains('8.676')
@@ -314,11 +461,8 @@ describe("Locality's coordinate selection map works", () => {
cy.contains('Central Finland').first().click() // first item on the list cy.contains('Central Finland').first().click() // first item on the list
cy.contains('Latitude: 63.05484, Longitude: 24.75291') cy.contains('Latitude: 63.05484, Longitude: 24.75291')
cy.contains('Save').click() cy.contains('Save').click()
cy.contains('Finalize entry').click()
cy.contains('button', 'Add existing reference').click() cy.addReferenceAndSave()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').click()
cy.visit(`/locality/20920?tab=1`) cy.visit(`/locality/20920?tab=1`)
cy.contains('63.05484') cy.contains('63.05484')
cy.contains('24.75291') cy.contains('24.75291')
+12 -38
View File
@@ -19,13 +19,7 @@ describe('Creating a species', () => {
cy.get('[id=species_name-textfield]').type('testSpecies') cy.get('[id=species_name-textfield]').type('testSpecies')
cy.get('[id=unique_identifier-textfield]').type('{backspace}testUniqueIdentifier') cy.get('[id=unique_identifier-textfield]').type('{backspace}testUniqueIdentifier')
cy.get('[id=write-button]').should('not.be.disabled') cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click() cy.addReferenceAndSave()
cy.get('[id=write-button]').should('be.disabled')
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
// tests that values are capitalised and trimmed properly // tests that values are capitalised and trimmed properly
cy.contains('Testsuperorder') cy.contains('Testsuperorder')
@@ -66,12 +60,7 @@ describe('Editing a species', () => {
cy.get('[id=subfamily_name-textfield]').type('heimo') cy.get('[id=subfamily_name-textfield]').type('heimo')
cy.get('[id=write-button]').should('not.be.disabled') cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click() cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('Saved species entry successfully.') cy.contains('Saved species entry successfully.')
cy.contains('Modifiedgenus') cy.contains('Modifiedgenus')
cy.contains('modifiedspecies') cy.contains('modifiedspecies')
@@ -101,14 +90,8 @@ describe('Editing a species', () => {
cy.get('[id=species_name-textfield]').type('newSpecies') cy.get('[id=species_name-textfield]').type('newSpecies')
cy.contains('Species: This field is required').should('not.exist') cy.contains('Species: This field is required').should('not.exist')
cy.get('[id=write-button]').click()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.addReferenceAndSave()
cy.contains('Saved species entry successfully.') cy.contains('Saved species entry successfully.')
cy.contains('Newgenus') cy.contains('Newgenus')
cy.contains('newspecies') cy.contains('newspecies')
@@ -126,8 +109,7 @@ describe('Taxonomy checks work', () => {
it('Creating a species that already exists in database does not work', () => { it('Creating a species that already exists in database does not work', () => {
cy.visit('/species/new') cy.visit('/species/new')
cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[id=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-21052]').click() cy.get('[data-cy=add-button-21052]').click()
cy.contains('Close').click() cy.contains('Close').click()
cy.get('[id=write-button]').click() cy.get('[id=write-button]').click()
@@ -136,8 +118,7 @@ describe('Taxonomy checks work', () => {
it('Invalid main taxonomy values do not work', () => { it('Invalid main taxonomy values do not work', () => {
cy.visit('/species/new') cy.visit('/species/new')
cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[id=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-21052]').click() cy.get('[data-cy=add-button-21052]').click()
cy.contains('Close').click() cy.contains('Close').click()
cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') // change identifier so it's not a duplicate species cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') // change identifier so it's not a duplicate species
@@ -158,8 +139,7 @@ describe('Taxonomy checks work', () => {
it('Invalid superorder/order pair does not work', () => { it('Invalid superorder/order pair does not work', () => {
cy.visit('/species/new') cy.visit('/species/new')
cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[id=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-21052]').click() cy.get('[data-cy=add-button-21052]').click()
cy.contains('Close').click() cy.contains('Close').click()
cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier')
@@ -172,8 +152,7 @@ describe('Taxonomy checks work', () => {
it('Invalid order/suborder/family trio does not work', () => { it('Invalid order/suborder/family trio does not work', () => {
cy.visit('/species/new') cy.visit('/species/new')
cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[id=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-84357]').click() cy.get('[data-cy=add-button-84357]').click()
cy.contains('Close').click() cy.contains('Close').click()
cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier')
@@ -187,8 +166,7 @@ describe('Taxonomy checks work', () => {
it('Invalid suborder/family pair does not work', () => { it('Invalid suborder/family pair does not work', () => {
cy.visit('/species/new') cy.visit('/species/new')
cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[id=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-84357]').click() cy.get('[data-cy=add-button-84357]').click()
cy.contains('Close').click() cy.contains('Close').click()
cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier')
@@ -201,8 +179,7 @@ describe('Taxonomy checks work', () => {
it('Invalid family/subfamily/genus trio does not work', () => { it('Invalid family/subfamily/genus trio does not work', () => {
cy.visit('/species/new') cy.visit('/species/new')
cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[id=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-23065]').click() cy.get('[data-cy=add-button-23065]').click()
cy.contains('Close').click() cy.contains('Close').click()
cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier')
@@ -216,8 +193,7 @@ describe('Taxonomy checks work', () => {
it('Invalid subfamily/genus pair does not work', () => { it('Invalid subfamily/genus pair does not work', () => {
cy.visit('/species/new') cy.visit('/species/new')
cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[id=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-23065]').click() cy.get('[data-cy=add-button-23065]').click()
cy.contains('Close').click() cy.contains('Close').click()
cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier')
@@ -230,8 +206,7 @@ describe('Taxonomy checks work', () => {
it('When genus is indet. species has to be indet.', () => { it('When genus is indet. species has to be indet.', () => {
cy.visit('/species/new') cy.visit('/species/new')
cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[id=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-21052]').click() cy.get('[data-cy=add-button-21052]').click()
cy.contains('Close').click() cy.contains('Close').click()
cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier')
@@ -246,8 +221,7 @@ describe('Taxonomy checks work', () => {
it('When genus is gen. species has to be sp.', () => { it('When genus is gen. species has to be sp.', () => {
cy.visit('/species/new') cy.visit('/species/new')
cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') cy.get('[data-cy=copy_existing_taxonomy_button]').click()
cy.get('[id=copy_existing_taxonomy_button]').click()
cy.get('[data-cy=add-button-21052]').click() cy.get('[data-cy=add-button-21052]').click()
cy.contains('Close').click() cy.contains('Close').click()
cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier')
+4 -13
View File
@@ -17,12 +17,8 @@ describe('Creating time bound', () => {
cy.get('[id=write-button]').should('be.disabled') cy.get('[id=write-button]').should('be.disabled')
cy.get('[id=age-textfield]').first().type('20') cy.get('[id=age-textfield]').first().type('20')
cy.contains('Age (Ma): This field is required').should('not.exist') cy.contains('Age (Ma): This field is required').should('not.exist')
cy.get('[id=write-button]').click()
cy.contains('button', 'Add existing reference').click() cy.addReferenceAndSave()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('Edited item successfully.') cy.contains('Edited item successfully.')
}) })
}) })
@@ -38,13 +34,8 @@ describe('Editing time bound', () => {
cy.get('[id=edit-button]').click() cy.get('[id=edit-button]').click()
cy.get('[id=age-textfield]').first().type('{backspace}{backspace}{backspace}{backspace}{backspace}1.5') cy.get('[id=age-textfield]').first().type('{backspace}{backspace}{backspace}{backspace}{backspace}1.5')
cy.contains('This field is required').should('not.exist') cy.contains('This field is required').should('not.exist')
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled') cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('Edited item successfully.') cy.contains('Edited item successfully.')
cy.contains('1.5') cy.contains('1.5')
}) })
+16 -56
View File
@@ -16,13 +16,8 @@ describe('Creating a time unit', () => {
cy.get('[data-cy=add-button-11]').first().click() cy.get('[data-cy=add-button-11]').first().click()
cy.get('[id=low_bnd-tableselection]').first().click() // Select Lower Bound Button cy.get('[id=low_bnd-tableselection]').first().click() // Select Lower Bound Button
cy.get('[data-cy=add-button-14]').first().click() cy.get('[data-cy=add-button-14]').first().click()
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled') cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('Testing Time Unit Creation') cy.contains('Testing Time Unit Creation')
cy.contains('ALMAAsianlandmammalage') cy.contains('ALMAAsianlandmammalage')
cy.contains('C2N-o') cy.contains('C2N-o')
@@ -130,13 +125,8 @@ describe('Creating a time unit', () => {
cy.get('[id=sequence-tableselection]').should('have.value', 'ALMAAsianlandmammalage') cy.get('[id=sequence-tableselection]').should('have.value', 'ALMAAsianlandmammalage')
cy.contains('C2N-o') cy.contains('C2N-o')
cy.contains('C2N-y') cy.contains('C2N-y')
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled') cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('C2N-o') cy.contains('C2N-o')
cy.contains('C2N-y') cy.contains('C2N-y')
cy.contains('Creating new time-unit').should('not.exist') cy.contains('Creating new time-unit').should('not.exist')
@@ -151,13 +141,8 @@ describe('Creating a time unit', () => {
cy.get('[data-cy=add-button-11]').first().click() cy.get('[data-cy=add-button-11]').first().click()
cy.get('[id=low_bnd-tableselection]').first().click() cy.get('[id=low_bnd-tableselection]').first().click()
cy.get('[data-cy=add-button-14]').first().click() cy.get('[data-cy=add-button-14]').first().click()
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled') cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('C2N-o') cy.contains('C2N-o')
cy.contains('C2N-y') cy.contains('C2N-y')
cy.get('[id=edit-button]').click() cy.get('[id=edit-button]').click()
@@ -165,13 +150,8 @@ describe('Creating a time unit', () => {
cy.get('[data-cy=add-button-CalatayudTeruellocalbiozone]').first().click() cy.get('[data-cy=add-button-CalatayudTeruellocalbiozone]').first().click()
cy.get('[id=low_bnd-tableselection]').first().click() cy.get('[id=low_bnd-tableselection]').first().click()
cy.get('[data-cy=add-button-49]').first().click() cy.get('[data-cy=add-button-49]').first().click()
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled') cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('CalatayudTeruellocalbiozone') cy.contains('CalatayudTeruellocalbiozone')
cy.contains('Langhian/Serravallian') cy.contains('Langhian/Serravallian')
}) })
@@ -189,13 +169,8 @@ describe('Editing a time unit', () => {
cy.get('[id=tu_display_name-textfield]').should('be.disabled') // name field is disabled when editing cy.get('[id=tu_display_name-textfield]').should('be.disabled') // name field is disabled when editing
cy.get('[id=sequence-tableselection]').first().click() cy.get('[id=sequence-tableselection]').first().click()
cy.get('[data-cy=add-button-ALMAAsianlandmammalage]').first().click() cy.get('[data-cy=add-button-ALMAAsianlandmammalage]').first().click()
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled') cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('ALMAAsianlandmammalage') cy.contains('ALMAAsianlandmammalage')
}) })
@@ -274,13 +249,8 @@ describe('Editing a time unit', () => {
cy.get('[id=sequence-tableselection]').should('have.value', 'CalatayudTeruellocalbiozone') cy.get('[id=sequence-tableselection]').should('have.value', 'CalatayudTeruellocalbiozone')
cy.contains('C2N-y') cy.contains('C2N-y')
cy.contains('MioceneLate-low') cy.contains('MioceneLate-low')
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled') cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('CalatayudTeruellocalbiozone') cy.contains('CalatayudTeruellocalbiozone')
cy.contains('C2N-y') cy.contains('C2N-y')
cy.contains('MioceneLate-low') cy.contains('MioceneLate-low')
@@ -292,13 +262,8 @@ describe('Editing a time unit', () => {
cy.get('[id=edit-button]').click() cy.get('[id=edit-button]').click()
cy.get('[id=sequence-tableselection]').first().click() cy.get('[id=sequence-tableselection]').first().click()
cy.get('[data-cy=add-button-easternparatethys]').first().click() cy.get('[data-cy=add-button-easternparatethys]').first().click()
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled') cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('Bahean') cy.contains('Bahean')
cy.contains('easternparatethys') cy.contains('easternparatethys')
cy.get('[id=edit-button]').click() cy.get('[id=edit-button]').click()
@@ -307,13 +272,8 @@ describe('Editing a time unit', () => {
cy.get('[data-cy=add-button-CalatayudTeruellocalbiozone]').first().click() cy.get('[data-cy=add-button-CalatayudTeruellocalbiozone]').first().click()
cy.get('[id=up_bnd-tableselection]').first().click() cy.get('[id=up_bnd-tableselection]').first().click()
cy.get('[data-cy=add-button-14]').first().click() cy.get('[data-cy=add-button-14]').first().click()
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled') cy.addReferenceAndSave()
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
cy.contains('CalatayudTeruellocalbiozone') cy.contains('CalatayudTeruellocalbiozone')
cy.contains('C2N-o') cy.contains('C2N-o')
}) })
+25
View File
@@ -42,3 +42,28 @@ Cypress.Commands.add('pageForbidden', url => {
Cypress.Commands.add('resetDatabase', () => { Cypress.Commands.add('resetDatabase', () => {
cy.task('resetDatabase') cy.task('resetDatabase')
}) })
// use this once you have edited some data and want to save it
Cypress.Commands.add('addReferenceAndSave', () => {
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').should('be.disabled')
cy.contains('button', 'Add existing reference').click()
cy.get('button[data-cy^="add-button"]').first().click()
cy.contains('button', 'Close').click()
cy.get('[id=write-button]').should('not.be.disabled')
cy.get('[id=write-button]').click()
})
Cypress.Commands.add('clearNewSpeciesForm', () => {
cy.get('[name=order_name]').clear()
cy.get('[name=family_name]').clear()
cy.get('[name=genus_name]').clear()
cy.get('[name=species_name]').clear()
cy.get('[name=subclass_or_superorder_name]').clear()
cy.get('[name=suborder_or_superfamily_name]').clear()
cy.get('[name=subfamily_name]').clear()
cy.get('[name=unique_identifier]').clear()
cy.get('[name=taxonomic_status]').clear()
cy.get('[name=sp_comment]').clear()
cy.get('[name=sp_author]').clear()
})
+188 -184
View File
@@ -1,12 +1,15 @@
Current reference types & their field labels / field names (29.09.2024) # Reference types
As of 29.09.2024 there is no UI to add more entries however you can do it by altering the db itself. Note: This file is old and should be checked and updated.
In the test_data/sqlfiles/now_test.sql code related to adding reference types is around rows 3665-3700 and the fields for the data types are added around rows 3175-3525. Current reference types & their field labels / field names
Do concider other options before you try to alter the data by hand. In the `test_data/sqlfiles/now_test.sql` file, code related to adding reference types is around rows 3665-3700 and the fields for the data types are added around rows 3175-3525.
Do consider other options before you try to alter the data by hand.
## Existing fields & their ids ## Existing fields & their ids
title_primary 1 title_primary 1
authors_primary 2 authors_primary 2
date_primary 3 date_primary 3
@@ -32,237 +35,238 @@ Do concider other options before you try to alter the data by hand.
exact_date 23 exact_date 23
"ref_type_id": 1, "ref_type": "Journal"' "ref_type_id": 1, "ref_type": "Journal"'
Title / title_primary Title / title_primary
Authors / authors_primary Authors / authors_primary
Year / date_primary Year / date_primary
Journal / journal_id Journal / journal_id
Volume / volume Volume / volume
Issue / issue Issue / issue
Start Page / start_page Start Page / start_page
End Page / end_page End Page / end_page
Publisher / publisher Publisher / publisher
City / pub_place City / pub_place
Title of Issue / title_secondary Title of Issue / title_secondary
Editors of Issue / authors_secondary Editors of Issue / authors_secondary
Series Title / title_series Series Title / title_series
ISSN/ISBN / issn_isbn ISSN/ISBN / issn_isbn
Abstract / ref_abstract Abstract / ref_abstract
Web/URL / web_url Web/URL / web_url
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary, journal Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary, journal
"ref_type_id": 2, "ref_type": "Book", "ref_type_id": 2, "ref_type": "Book",
Title / title_primary Title / title_primary
Authors / authors_primary Authors / authors_primary
Year / date_primary Year / date_primary
Edition / volume Edition / volume
Volume / issue Volume / issue
No of Pages / end_page No of Pages / end_page
Publisher / publisher Publisher / publisher
City / pub_place City / pub_place
Editors / authors_secondary Editors / authors_secondary
Series Title / title_series Series Title / title_series
Series Editors / authors_series Series Editors / authors_series
ISBN / issn_isbn ISBN / issn_isbn
Abstract / ref_abstract Abstract / ref_abstract
Web/URL / web_url Web/URL / web_url
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Mandatory fields: title_primary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series Mandatory fields: title_primary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series
"ref_type_id": 3, "ref_type": Book Chapter' "ref_type_id": 3, "ref_type": Book Chapter'
Title / title_primary Title / title_primary
Authors / authors_primary Authors / authors_primary
Year / date_primary Year / date_primary
Edition / volume Edition / volume
Chapter No / issue Chapter No / issue
Start Page / start_page Start Page / start_page
End Page / end_page End Page / end_page
Publisher / publisher Publisher / publisher
City / pub_place City / pub_place
Book Title / title_secondary Book Title / title_secondary
Editors / authors_secondary Editors / authors_secondary
Series Title / title_series Series Title / title_series
Series Editors / authors_series Series Editors / authors_series
ISBN / issn_isbn ISBN / issn_isbn
Abstract / ref_abstract Abstract / ref_abstract
Web/URL / web_url Web/URL / web_url
Volume / misc_2 Volume / misc_2
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series
"ref_type_id": 4, "ref_type": Thesis/Dissertation' "ref_type_id": 4, "ref_type": Thesis/Dissertation'
Title / title_primary Title / title_primary
Authors / authors_primary Authors / authors_primary
Year / date_primary Year / date_primary
No of Pages / end_page No of Pages / end_page
Institution / publisher Institution / publisher
Abstract / ref_abstract Abstract / ref_abstract
Web/URL / web_url Web/URL / web_url
Degree / misc_1 Degree / misc_1
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary
"ref_type_id": 5, "ref_type": Conference Proceeding' "ref_type_id": 5, "ref_type": Conference Proceeding'
Title / title_primary Title / title_primary
Authors / authors_primary Authors / authors_primary
Publication Year / date_primary Publication Year / date_primary
Journal / journal_id Journal / journal_id
Volume / volume Volume / volume
Edition / issue Edition / issue
Start Page / start_page Start Page / start_page
End Page / end_page End Page / end_page
Publisher / publisher Publisher / publisher
City / pub_place City / pub_place
Title of Conference / title_secondary Title of Conference / title_secondary
Editors / authors_secondary Editors / authors_secondary
Year of Conference / date_secondary Year of Conference / date_secondary
Series Title / title_series Series Title / title_series
Series Editors / authors_series Series Editors / authors_series
Abstract / ref_abstract Abstract / ref_abstract
Web/URL / web_url Web/URL / web_url
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series, journal Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series, journal
"ref_type_id": 6, "ref_type": Electronic Citation' "ref_type_id": 6, "ref_type": Electronic Citation'
Title / title_primary Title / title_primary
Authors / authors_primary Authors / authors_primary
Last Update (Year) / date_primary Last Update (Year) / date_primary
Organisation / title_secondary Organisation / title_secondary
Editors / authors_secondary Editors / authors_secondary
Abstract / ref_abstract Abstract / ref_abstract
Web/URL / web_url Web/URL / web_url
Last Update (Day Month) / misc_1 Last Update (Day Month) / misc_1
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Access Date / exact_date Access Date / exact_date
Mandatory fields: title_primary OR title_secondary OR gen_notes, date_primary, authors_primary OR authors_secondary, exact_date Mandatory fields: title_primary OR title_secondary OR gen_notes, date_primary, authors_primary OR authors_secondary, exact_date
"ref_type_id": 7, "ref_type": Internet Communication' "ref_type_id": 7, "ref_type": Internet Communication'
Subject / title_primary Subject / title_primary
Sender / authors_primary Sender / authors_primary
Year / date_primary Year / date_primary
Recipient / authors_secondary Recipient / authors_secondary
Sender's e-mail / misc_1 Sender's e-mail / misc_1
Recipient's e-mail / misc_2 Recipient's e-mail / misc_2
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Date of Message / exact_date Date of Message / exact_date
Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary OR authors_secondary, exact_date Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary OR authors_secondary, exact_date
"ref_type_id": 8, "ref_type": Report' "ref_type_id": 8, "ref_type": Report'
Title / title_primary Title / title_primary
Authors / authors_primary Authors / authors_primary
Year / date_primary Year / date_primary
Report Number / volume Report Number / volume
Start Page / start_page Start Page / start_page
End Page / end_page End Page / end_page
Publisher / publisher Publisher / publisher
Pub Place / pub_place Pub Place / pub_place
Report Name / title_secondary Report Name / title_secondary
Editors / authors_secondary Editors / authors_secondary
Series Title / title_series Series Title / title_series
Series Editors / authors_series Series Editors / authors_series
Abstract / ref_abstract Abstract / ref_abstract
Web/URL / web_url Web/URL / web_url
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series
"ref_type_id": 9, "ref_type": Unpublished Work' "ref_type_id": 9, "ref_type": Unpublished Work'
Title / title_primary Title / title_primary
Authors / authors_primary Authors / authors_primary
Year / date_primary Year / date_primary
Organisation / title_secondary Organisation / title_secondary
Editors / authors_secondary Editors / authors_secondary
Series Title / title_series Series Title / title_series
Series Editors / authors_series Series Editors / authors_series
Abstract / ref_abstract Abstract / ref_abstract
Web/URL / web_url Web/URL / web_url
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series
"ref_type_id": 10, "ref_type": Personal Communication' "ref_type_id": 10, "ref_type": Personal Communication'
Authors / authors_primary Authors / authors_primary
Year / date_primary Year / date_primary
Recipients / authors_secondary Recipients / authors_secondary
Type / misc_1 Type / misc_1
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Date Sent / exact_date Date Sent / exact_date
Mandatory fields: gen_notes, date_primary, authors_primary OR authors_secondary, exact_date Mandatory fields: gen_notes, date_primary, authors_primary OR authors_secondary, exact_date
"ref_type_id": 11, "ref_type": Manuscript' "ref_type_id": 11, "ref_type": Manuscript'
Title / title_primary Title / title_primary
Authors / authors_primary Authors / authors_primary
Date / date_primary Date / date_primary
Organisation / title_secondary Organisation / title_secondary
Editors / authors_secondary Editors / authors_secondary
Series Title / title_series Series Title / title_series
Series Editors / authors_series Series Editors / authors_series
Notes / gen_notes Notes / gen_notes
Language / printed_language Language / printed_language
Date / exact_date Date / exact_date
Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series, exact_date Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series, exact_date
"ref_type_id": 12, "ref_type": Notes' "ref_type_id": 12, "ref_type": Notes'
Subject / title_primary Subject / title_primary
Authors / authors_primary Authors / authors_primary
Year / date_primary Year / date_primary
Notes / gen_notes Notes / gen_notes
Date / exact_date Date / exact_date
Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary, exact_date Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary, exact_date
"ref_type_id": 13, "ref_type": Editing' "ref_type_id": 13, "ref_type": Editing'
Title / title_primary Title / title_primary
Authors / authors_primary Authors / authors_primary
Year / date_primary Year / date_primary
Notes / gen_notes Notes / gen_notes
Date / exact_date Date / exact_date
Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary, exact_date Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary, exact_date
"ref_type_id": 14, "ref_type": Undefined' "ref_type_id": 14, "ref_type": Undefined'
title_primary / title_primary title_primary / title_primary
authors_primary / authors_primary authors_primary / authors_primary
date_primary / date_primary date_primary / date_primary
journal_id / journal_id journal_id / journal_id
volume / volume volume / volume
issue / issue issue / issue
start_page / start_page start_page / start_page
end_page / end_page end_page / end_page
publisher / publisher publisher / publisher
pub_place / pub_place pub_place / pub_place
title_secondary / title_secondary title_secondary / title_secondary
authors_secondary / authors_secondary authors_secondary / authors_secondary
date_secondary / date_secondary date_secondary / date_secondary
title_series / title_series title_series / title_series
authors_series / authors_series authors_series / authors_series
issn_isbn / issn_isbn issn_isbn / issn_isbn
abstract / abstract abstract / abstract
web_url / web_url web_url / web_url
misc_1 / misc_1 misc_1 / misc_1
misc_2 / misc_2 misc_2 / misc_2
gen_notes / gen_notes gen_notes / gen_notes
language / language language / language
exact_date / exact_date exact_date / exact_date
Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series, exact_date, journal Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series, exact_date, journal
* In old php test version instead of journal, journal_id had to be defined. That seems odd, since when creating a new journal, journal_id will not initally be defined. So that will not be done here.
- In old php test version instead of journal, journal_id had to be defined. That seems odd, since when creating a new journal, journal_id will not initially be defined. So that will not be done here.
+1 -1
View File
@@ -1,4 +1,4 @@
# Localities # Species
## Required fields ## Required fields
+1 -1
View File
@@ -1,4 +1,4 @@
### Time Bound ### Time Bounds
A Time Bound can be given the following information: A Time Bound can be given the following information:
+1 -1
View File
@@ -1,4 +1,4 @@
### Time Unit ### Time Units
A Time Unit can be given the following information: A Time Unit can be given the following information:
@@ -3,6 +3,7 @@ import { EditingModal } from './EditingModal'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { Editable, EditDataType } from '@/shared/types' import { Editable, EditDataType } from '@/shared/types'
import { useDetailContext } from '../Context/DetailContext' import { useDetailContext } from '../Context/DetailContext'
import { useEffect } from 'react'
export type EditingFormField = { name: string; label: string; required?: boolean; type?: 'number' | 'string' } export type EditingFormField = { name: string; label: string; required?: boolean; type?: 'number' | 'string' }
@@ -11,6 +12,10 @@ Renders a button, that will open EditingModal, which can be used
to add a new entry to a list, or edit existing one. to add a new entry to a list, or edit existing one.
If using for adding new, provide arrayFieldName. If using for adding new, provide arrayFieldName.
For editing existing row, use existingObject and editAction. For editing existing row, use existingObject and editAction.
If you want to autofill the form's fields once the user e.g. clicks a button,
pass the updated values into the replacedValues prop. The form will be reset and uses those values.
The copyTaxonomyButton prop is used only in Locality -> Species tab.
*/ */
export const EditingForm = <T extends object, ParentType extends object>({ export const EditingForm = <T extends object, ParentType extends object>({
dataCy, dataCy,
@@ -19,6 +24,8 @@ export const EditingForm = <T extends object, ParentType extends object>({
editAction, editAction,
existingObject, existingObject,
arrayFieldName, arrayFieldName,
replacedValues,
copyTaxonomyButton,
}: { }: {
dataCy?: string dataCy?: string
buttonText: string buttonText: string
@@ -26,15 +33,22 @@ export const EditingForm = <T extends object, ParentType extends object>({
arrayFieldName?: keyof ParentType arrayFieldName?: keyof ParentType
editAction?: (newObject: T) => void editAction?: (newObject: T) => void
existingObject?: T | undefined existingObject?: T | undefined
replacedValues?: T | undefined
copyTaxonomyButton?: JSX.Element | undefined
}) => { }) => {
const getDefaultValues: () => { [x: string]: unknown } = () => { const getDefaultValues: () => { [x: string]: unknown } = () => {
if (!existingObject) return {} if (!existingObject) return {}
return existingObject return existingObject
} }
const { register, trigger, formState, getValues } = useForm({ defaultValues: getDefaultValues() }) const { register, trigger, formState, getValues, reset } = useForm({ defaultValues: getDefaultValues() })
const { errors } = formState const { errors } = formState
const { editData, setEditData } = useDetailContext<ParentType>() const { editData, setEditData } = useDetailContext<ParentType>()
useEffect(() => {
if (replacedValues) reset(replacedValues)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [replacedValues])
const onSave = async () => { const onSave = async () => {
const result = await trigger() const result = await trigger()
if (!result) return false if (!result) return false
@@ -52,9 +66,11 @@ export const EditingForm = <T extends object, ParentType extends object>({
return ( return (
<EditingModal {...{ dataCy, buttonText, onSave }}> <EditingModal {...{ dataCy, buttonText, onSave }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
{copyTaxonomyButton}
{formFields.map(field => ( {formFields.map(field => (
<TextField <TextField
key={field.name} key={field.name}
slotProps={{ inputLabel: { shrink: true } }}
{...register(field.name, { {...register(field.name, {
required: field.required ? 'This field is required' : false, required: field.required ? 'This field is required' : false,
...(field.type === 'number' && { ...(field.type === 'number' && {
@@ -12,6 +12,7 @@ give an array in prop "selectedValues" that contains the string values
that already exist in editdata that already exist in editdata
*/ */
export const SelectingTable = <T extends MRT_RowData, ParentType extends object>({ export const SelectingTable = <T extends MRT_RowData, ParentType extends object>({
dataCy,
buttonText, buttonText,
data, data,
columns, columns,
@@ -24,6 +25,7 @@ export const SelectingTable = <T extends MRT_RowData, ParentType extends object>
isError, isError,
useObject, useObject,
}: { }: {
dataCy?: string
buttonText: string buttonText: string
data: T[] | undefined data: T[] | undefined
columns: MRT_ColumnDef<T>[] columns: MRT_ColumnDef<T>[]
@@ -71,7 +73,7 @@ export const SelectingTable = <T extends MRT_RowData, ParentType extends object>
if (isError) return <Box>Error fetching data for the selecting table.</Box> if (isError) return <Box>Error fetching data for the selecting table.</Box>
if (!data) return <CircularProgress /> if (!data) return <CircularProgress />
return ( return (
<EditingModal buttonText={buttonText}> <EditingModal buttonText={buttonText} dataCy={dataCy}>
{data ? ( {data ? (
<TableView<T> <TableView<T>
data={filteredData} data={filteredData}
@@ -119,7 +119,7 @@ export const emptyLocality = {
} as unknown as LocalityDetailsType } as unknown as LocalityDetailsType
export const emptySpecies = { export const emptySpecies = {
class_name: '', class_name: 'Mammalia',
order_name: '', order_name: '',
family_name: '', family_name: '',
subclass_or_superorder_name: '', subclass_or_superorder_name: '',
@@ -128,7 +128,7 @@ export const emptySpecies = {
genus_name: '', genus_name: '',
species_name: '', species_name: '',
unique_identifier: '-', unique_identifier: '-',
taxonomic_status: null, taxonomic_status: '',
common_name: '', common_name: '',
sp_author: '', sp_author: '',
strain: null, strain: null,
@@ -8,12 +8,13 @@ import { useDetailContext } from './Context/DetailContext'
import { EditDataType, Editable, Reference, Species } from '@/shared/types' import { EditDataType, Editable, Reference, Species } from '@/shared/types'
import { useState, useEffect, Fragment } from 'react' import { useState, useEffect, Fragment } from 'react'
import { referenceValidator } from '@/shared/validators/validator' import { referenceValidator } from '@/shared/validators/validator'
import { checkTaxonomy, convertTaxonomyFields } from '../../util/taxonomyFunctions' import { checkSpeciesTaxonomy, convertSpeciesTaxonomyFields } from '../../util/taxonomyUtilities'
import { useLazyGetAllSpeciesQuery, useLazyGetAllSynonymsQuery } from '@/redux/speciesReducer' import { useGetAllSpeciesQuery, useGetAllSynonymsQuery } from '@/redux/speciesReducer'
import { useNotify } from '@/hooks/notification' import { useNotify } from '@/hooks/notification'
import { countryBoundingBoxes } from '@/country_data/countryBoundingBoxes' import { countryBoundingBoxes } from '@/country_data/countryBoundingBoxes'
import { boundingBoxSplit, isPointInBoxes } from '@/util/isPointInBox' import { boundingBoxSplit, isPointInBoxes } from '@/util/isPointInBox'
import { OutOfBoundsWarningModal, OutOfBoundsWarningModalState } from './OutOfBoundsWarningModal' import { OutOfBoundsWarningModal, OutOfBoundsWarningModalState } from './OutOfBoundsWarningModal'
import { skipToken } from '@reduxjs/toolkit/query'
export const WriteButton = <T,>({ export const WriteButton = <T,>({
onWrite, onWrite,
@@ -28,8 +29,8 @@ export const WriteButton = <T,>({
useDetailContext<T>() useDetailContext<T>()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { notify } = useNotify() const { notify } = useNotify()
const [getSpeciesData] = useLazyGetAllSpeciesQuery() const { data: speciesData } = useGetAllSpeciesQuery(!taxonomy ? skipToken : undefined)
const [getSynonyms] = useLazyGetAllSynonymsQuery() const { data: synonymData } = useGetAllSynonymsQuery(!taxonomy ? skipToken : undefined)
// For coordinate out of bounds warning // For coordinate out of bounds warning
const [warningModalState, setWarningModalState] = useState<OutOfBoundsWarningModalState>({ const [warningModalState, setWarningModalState] = useState<OutOfBoundsWarningModalState>({
@@ -50,6 +51,11 @@ export const WriteButton = <T,>({
return !isPointInBoxes(dec_lat, dec_long, boundingBoxSplit(countryBoundingBoxes[country])) return !isPointInBoxes(dec_lat, dec_long, boundingBoxSplit(countryBoundingBoxes[country]))
} }
useEffect(() => {
if (taxonomy && (!speciesData || !synonymData)) setLoading(true)
else setLoading(false)
}, [speciesData, synonymData])
/* validates all fields when entering new/editing mode /* validates all fields when entering new/editing mode
to disable write button when unvisited tabs have to disable write button when unvisited tabs have
validation errors (e.g. missing required fields) */ validation errors (e.g. missing required fields) */
@@ -107,23 +113,21 @@ export const WriteButton = <T,>({
// @ts-expect-error Reason: Typescript doesn't recognise that references do exist. Unable to find a way around it. Fix if extra time // @ts-expect-error Reason: Typescript doesn't recognise that references do exist. Unable to find a way around it. Fix if extra time
}, [mode, editData.references]) }, [mode, editData.references])
const writeWithTaxonomyCheck = async () => { const writeWithTaxonomyCheck = () => {
let speciesEditData: EditDataType<Species> | undefined = undefined let speciesEditData: EditDataType<Species> | undefined = undefined
if (!mode.staging) { if (!mode.staging) {
const { data: speciesData } = await getSpeciesData(undefined, true)
const { data: synonyms } = await getSynonyms(undefined, true)
if (!speciesData) { if (!speciesData) {
notify('Could not fetch species to check taxonomy data.', 'error') notify('Could not fetch species to check taxonomy data.', 'error')
return return
} }
if (!synonyms) { if (!synonymData) {
notify('Could not fetch synonyms to check taxonomy data.', 'error') notify('Could not fetch synonyms to check taxonomy data.', 'error')
return return
} }
// converts taxonomy fields to capitalized/lowercased // converts taxonomy fields to capitalized/lowercased
speciesEditData = convertTaxonomyFields(editData as EditDataType<Species>) speciesEditData = convertSpeciesTaxonomyFields(editData as EditDataType<Species>)
const errors = checkTaxonomy(speciesEditData, speciesData, synonyms) const errors = checkSpeciesTaxonomy(speciesEditData, speciesData, synonymData)
if (errors.size > 0) { if (errors.size > 0) {
const errorMessage = [...errors].reduce((acc, currentError) => acc + `\n${currentError}`) const errorMessage = [...errors].reduce((acc, currentError) => acc + `\n${currentError}`)
notify(errorMessage, 'error', null) notify(errorMessage, 'error', null)
@@ -144,10 +148,10 @@ export const WriteButton = <T,>({
}) })
} }
const handleWriteButtonClick = async () => { const handleWriteButtonClick = () => {
if (taxonomy) { if (taxonomy) {
setLoading(true) setLoading(true)
await writeWithTaxonomyCheck() writeWithTaxonomyCheck()
setLoading(false) setLoading(false)
return return
} }
@@ -186,13 +190,13 @@ export const WriteButton = <T,>({
!('dec_long' in (editData as object)) || !('dec_long' in (editData as object)) ||
!('country' in (editData as object)) !('country' in (editData as object))
) { ) {
void handleWriteButtonClick() handleWriteButtonClick()
return return
} }
const localityObject = editData as unknown as { dec_lat: number; dec_long: number; country: string } const localityObject = editData as unknown as { dec_lat: number; dec_long: number; country: string }
if (!isCoordinateOutOfBounds(localityObject.dec_lat, localityObject.dec_long, localityObject.country)) { if (!isCoordinateOutOfBounds(localityObject.dec_lat, localityObject.dec_long, localityObject.country)) {
void handleWriteButtonClick() handleWriteButtonClick()
return return
} }
@@ -1,17 +1,104 @@
import { LocalityDetailsType, LocalitySpecies, Species, SpeciesDetailsType } from '@/shared/types' import {
Editable,
EditDataType,
LocalityDetailsType,
LocalitySpecies,
Species,
SpeciesDetailsType,
} from '@/shared/types'
import { EditableTable } from '@/components/DetailView/common/EditableTable' import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { EditingForm } from '@/components/DetailView/common/EditingForm' import { EditingForm } from '@/components/DetailView/common/EditingForm'
import { SelectingTable } from '@/components/DetailView/common/SelectingTable' import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers' import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext' import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { useGetAllSpeciesQuery } from '@/redux/speciesReducer' import { useGetAllSpeciesQuery } from '@/redux/speciesReducer'
import { Box } from '@mui/material' import { Box, CircularProgress } from '@mui/material'
import { skipToken } from '@reduxjs/toolkit/query' import { skipToken } from '@reduxjs/toolkit/query'
import { MRT_ColumnDef } from 'material-react-table' import { MRT_ColumnDef } from 'material-react-table'
import {
checkSpeciesTaxonomy,
convertSpeciesTaxonomyFields,
fixNullValuesInTaxonomyFields,
} from '@/util/taxonomyUtilities'
import { useNotify } from '@/hooks/notification'
import { validateSpecies } from '@/shared/validators/species'
import { smallSpeciesTableColumns } from '@/common'
import { useState } from 'react'
import { SynonymsModal } from '@/components/Species/SynonymsModal'
export const SpeciesTab = () => { export const SpeciesTab = () => {
const { mode, editData, setEditData } = useDetailContext<LocalityDetailsType>() const { mode, editData, setEditData } = useDetailContext<LocalityDetailsType>()
const { data: speciesData, isError } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined) const { data: speciesData, isError } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined)
const { notify } = useNotify()
const [replacedValues, setReplacedValues] = useState<EditDataType<Species> | undefined>()
const [selectedSpecies, setSelectedSpecies] = useState<string | undefined>()
const [modalOpen, setModalOpen] = useState<boolean>(false)
const handleRowActionClick = (row: Species) => {
setSelectedSpecies(row.species_id.toString())
setModalOpen(true)
}
const copyTaxonomyButton = (
<Box key="copy_existing_taxonomy_button">
<SelectingTable<Species, Species>
dataCy="copy_existing_taxonomy_button"
buttonText="Copy existing taxonomy"
data={speciesData}
isError={isError}
columns={smallSpeciesTableColumns}
fieldName="order_name" // this doesn't do anything here but is required
idFieldName="species_id"
useObject={true}
tableRowAction={handleRowActionClick}
editingAction={(selectedSpecies: Species) => {
const fixedSpecies = fixNullValuesInTaxonomyFields(selectedSpecies)
setReplacedValues({
subclass_or_superorder_name: fixedSpecies.subclass_or_superorder_name,
order_name: fixedSpecies.order_name!,
suborder_or_superfamily_name: fixedSpecies.suborder_or_superfamily_name,
family_name: fixedSpecies.family_name!,
subfamily_name: fixedSpecies.subfamily_name,
genus_name: fixedSpecies.genus_name!,
species_name: fixedSpecies.species_name!,
unique_identifier: fixedSpecies.unique_identifier!,
taxonomic_status: '',
sp_comment: '',
sp_author: '',
})
}}
/>
<SynonymsModal open={modalOpen} onClose={() => setModalOpen(false)} selectedSpecies={selectedSpecies} />
</Box>
)
const convertAndCheckNewSpeciesTaxonomy = (newSpecies: EditDataType<Species>) => {
const errors = []
for (const field in newSpecies) {
const errorObject = validateSpecies(
newSpecies as unknown as EditDataType<SpeciesDetailsType>,
field as unknown as keyof EditDataType<SpeciesDetailsType>
)
const { error } = errorObject
if (error) errors.push(errorObject)
}
if (errors.length > 0) {
notify('Following validators failed: \n' + errors.map(e => `${e.name}: ${e.error}`).join('\n'), 'error')
return false
}
const convertedSpecies = convertSpeciesTaxonomyFields(newSpecies)
const everyLs = editData.now_ls.map(ls => ls.com_species) as unknown as Editable<Species>[]
const filteredLs = everyLs.filter(species => species.rowState === 'new')
const taxonomyErrors = checkSpeciesTaxonomy(convertedSpecies, speciesData!.concat(filteredLs), [])
if (taxonomyErrors.size > 0) {
const errorMessage = [...taxonomyErrors].reduce((acc, currentError) => acc + `\n${currentError}`)
notify(errorMessage, 'error', null)
return false
}
return convertedSpecies
}
const speciesColumns: MRT_ColumnDef<Species>[] = [ const speciesColumns: MRT_ColumnDef<Species>[] = [
{ {
@@ -80,6 +167,14 @@ export const SpeciesTab = () => {
accessorKey: 'com_species.taxonomic_status', accessorKey: 'com_species.taxonomic_status',
header: 'Taxon status', header: 'Taxon status',
}, },
{
accessorKey: 'com_species.sp_comment',
header: 'Comment',
},
{
accessorKey: 'com_species.sp_author',
header: 'Author',
},
] ]
const formFields: { name: string; label: string; required?: boolean }[] = [ const formFields: { name: string; label: string; required?: boolean }[] = [
{ name: 'order_name', label: 'Order', required: true }, { name: 'order_name', label: 'Order', required: true },
@@ -88,26 +183,36 @@ export const SpeciesTab = () => {
{ name: 'species_name', label: 'Species', required: true }, { name: 'species_name', label: 'Species', required: true },
{ name: 'subclass_or_superorder_name', label: 'Subclass or Superorder' }, { name: 'subclass_or_superorder_name', label: 'Subclass or Superorder' },
{ name: 'suborder_or_superfamily_name', label: 'Suborder or Superfamily' }, { name: 'suborder_or_superfamily_name', label: 'Suborder or Superfamily' },
{ name: 'subfamily_name', label: 'Subfamily or Tribe' },
{ name: 'unique_identifier', label: 'Unique Identifier', required: true }, { name: 'unique_identifier', label: 'Unique Identifier', required: true },
{ name: 'taxonomic_status', label: 'Taxon status' }, { name: 'taxonomic_status', label: 'Taxon status' },
{ name: 'sp_comment', label: 'Comment' },
{ name: 'sp_author', label: 'Author' },
] ]
if (!mode.read && !speciesData) return <CircularProgress />
return ( return (
<Grouped title="Species"> <Grouped title="Species">
{!mode.read && ( {!mode.read && (
<Box display="flex" gap={1}> <Box display="flex" gap={1}>
<EditingForm<Species, LocalityDetailsType> <EditingForm<EditDataType<Species>, LocalityDetailsType>
buttonText="Add new Species" buttonText="Add new Species"
formFields={formFields} formFields={formFields}
editAction={(newSpecies: Species) => { existingObject={{ unique_identifier: '-' }}
replacedValues={replacedValues}
copyTaxonomyButton={copyTaxonomyButton}
editAction={(newSpecies: EditDataType<Species>) => {
const convertedSpecies = convertAndCheckNewSpeciesTaxonomy(newSpecies)
if (!convertedSpecies) return
setEditData({ setEditData({
...editData, ...editData,
now_ls: [ now_ls: [
...editData.now_ls, ...editData.now_ls,
{ {
lid: editData.lid, lid: editData.lid,
species_id: newSpecies.species_id, species_id: undefined,
com_species: { ...(newSpecies as unknown as SpeciesDetailsType) }, com_species: { ...(convertedSpecies as unknown as SpeciesDetailsType), class_name: 'Mammalia' },
rowState: 'new', rowState: 'new',
}, },
], ],
@@ -130,7 +235,9 @@ export const SpeciesTab = () => {
{ {
lid: editData.lid, lid: editData.lid,
species_id: newSpecies.species_id, species_id: newSpecies.species_id,
com_species: { ...(newSpecies as unknown as SpeciesDetailsType) }, com_species: {
...(fixNullValuesInTaxonomyFields(newSpecies) as SpeciesDetailsType),
},
rowState: 'new', rowState: 'new',
}, },
], ],
@@ -16,6 +16,7 @@ import { validateSpecies } from '@/shared/validators/species'
import { emptySpecies } from '../DetailView/common/defaultValues' import { emptySpecies } from '../DetailView/common/defaultValues'
import { useNotify } from '@/hooks/notification' import { useNotify } from '@/hooks/notification'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { fixNullValuesInTaxonomyFields } from '@/util/taxonomyUtilities'
export const SpeciesDetails = () => { export const SpeciesDetails = () => {
const { id } = useParams() const { id } = useParams()
@@ -40,15 +41,11 @@ export const SpeciesDetails = () => {
}, [deleteSuccess, deleteError, notify, navigate]) }, [deleteSuccess, deleteError, notify, navigate])
useEffect(() => { useEffect(() => {
// changes subclass, suborder, and subfamily from null to empty string to make validators work // changes subclass, suborder, subfamily and taxonomic_status from null to empty string to make validators work
// this should be removed if validators are improved in the future // this should be removed if validators are improved in the future
if (data) { if (data) {
setModifiedData({ const fixedSpeciesData = fixNullValuesInTaxonomyFields(data)
...data, setModifiedData(fixedSpeciesData as SpeciesDetailsType)
subclass_or_superorder_name: data.subclass_or_superorder_name ?? '',
suborder_or_superfamily_name: data.suborder_or_superfamily_name ?? '',
subfamily_name: data.subfamily_name ?? '',
})
} }
}, [data]) }, [data])
@@ -4,9 +4,66 @@ import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { MRT_ColumnDef } from 'material-react-table' import { MRT_ColumnDef } from 'material-react-table'
import { EditableTable } from '@/components/DetailView/common/EditableTable' import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { EditingForm, EditingFormField } from '@/components/DetailView/common/EditingForm' import { EditingForm, EditingFormField } from '@/components/DetailView/common/EditingForm'
import { useNotify } from '@/hooks/notification'
import { validateSynonym } from '@/shared/validators/synonym'
import { convertSynonymTaxonomyFields } from '@/util/taxonomyUtilities'
import { useGetAllSpeciesQuery } from '@/redux/speciesReducer'
import { skipToken } from '@reduxjs/toolkit/query'
import { CircularProgress } from '@mui/material'
export const SynonymTab = () => { export const SynonymTab = () => {
const { mode, setEditData, editData } = useDetailContext<SpeciesDetailsType>() const { mode, setEditData, editData } = useDetailContext<SpeciesDetailsType>()
const { data: speciesQueryData, isFetching } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined)
const { notify } = useNotify()
const convertAndCheckNewSynonymTaxonomy = (newSynonym: EditDataType<SpeciesSynonym>) => {
const errors = []
for (const field in newSynonym) {
const errorObject = validateSynonym(
newSynonym as unknown as EditDataType<SpeciesSynonym>,
field as unknown as keyof EditDataType<SpeciesSynonym>
)
const { error } = errorObject
if (error) errors.push(errorObject)
}
if (errors.length > 0) {
notify('Following validators failed: \n' + errors.map(e => `${e.name}: ${e.error}`).join('\n'), 'error')
return false
}
const convertedSynonym = convertSynonymTaxonomyFields(newSynonym)
for (const existingSynonym of editData.com_taxa_synonym) {
if (
convertedSynonym.syn_genus_name === existingSynonym.syn_genus_name &&
convertedSynonym.syn_species_name === existingSynonym.syn_species_name
) {
notify(
`${convertedSynonym.syn_genus_name} ${convertedSynonym.syn_species_name} \
has already been added as a synonym for this species.`,
'error',
null
)
return false
}
}
for (const existingSpecies of speciesQueryData!) {
if (
convertedSynonym.syn_genus_name === existingSpecies.genus_name &&
convertedSynonym.syn_species_name === existingSpecies.species_name
) {
notify(
`${convertedSynonym.syn_genus_name} ${convertedSynonym.syn_species_name} \
has already been created as a separate species, adding it as a synonym is currently not allowed.`,
'error',
null
)
return false
}
}
return convertedSynonym
}
const columns: MRT_ColumnDef<SpeciesSynonym>[] = [ const columns: MRT_ColumnDef<SpeciesSynonym>[] = [
{ {
@@ -31,15 +88,17 @@ export const SynonymTab = () => {
const editingForm = ( const editingForm = (
<EditingForm<Species, SpeciesDetailsType> <EditingForm<Species, SpeciesDetailsType>
buttonText="Add new Species" buttonText="Add synonym"
formFields={formFields} formFields={formFields}
editAction={(newSynonym: EditDataType<SpeciesSynonym>) => { editAction={(newSynonym: EditDataType<SpeciesSynonym>) => {
const convertedSynonym = convertAndCheckNewSynonymTaxonomy(newSynonym)
if (!convertedSynonym) return
setEditData({ setEditData({
...editData, ...editData,
com_taxa_synonym: [ com_taxa_synonym: [
...editData.com_taxa_synonym, ...editData.com_taxa_synonym,
{ {
...newSynonym, ...convertedSynonym,
rowState: 'new', rowState: 'new',
}, },
], ],
@@ -48,6 +107,8 @@ export const SynonymTab = () => {
/> />
) )
if (isFetching) return <CircularProgress />
return ( return (
<> <>
<Grouped title="Synonyms"> <Grouped title="Synonyms">
@@ -1,13 +1,14 @@
import { SpeciesDetailsType, Species } from '@/shared/types' import { SpeciesDetailsType, Species } from '@/shared/types'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers' import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext' import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { emptyOption } from '@/components/DetailView/common/misc' import { fixNullValuesInTaxonomyFields } from '@/util/taxonomyUtilities'
import { Box } from '@mui/material' import { Box } from '@mui/material'
import { SelectingTable } from '@/components/DetailView/common/SelectingTable' import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
import { useGetAllSpeciesQuery } from '@/redux/speciesReducer' import { useGetAllSpeciesQuery } from '@/redux/speciesReducer'
import { smallSpeciesTableColumns } from '@/common' import { smallSpeciesTableColumns } from '@/common'
import { SynonymsModal } from '../SynonymsModal' import { SynonymsModal } from '../SynonymsModal'
import { useState } from 'react' import { useState } from 'react'
import { taxonStatusOptions } from '@/shared/taxonStatusOptions'
export const TaxonomyTab = () => { export const TaxonomyTab = () => {
const { textField, dropdown, bigTextField, editData, setEditData, mode } = useDetailContext<SpeciesDetailsType>() const { textField, dropdown, bigTextField, editData, setEditData, mode } = useDetailContext<SpeciesDetailsType>()
@@ -21,8 +22,9 @@ export const TaxonomyTab = () => {
} }
const copyTaxonomyButton = ( const copyTaxonomyButton = (
<Box key="copy_existing_taxonomy_button" id="copy_existing_taxonomy_button"> <Box key="copy_existing_taxonomy_button">
<SelectingTable<Species, Species> <SelectingTable<Species, Species>
dataCy="copy_existing_taxonomy_button"
buttonText="Copy existing taxonomy" buttonText="Copy existing taxonomy"
data={speciesQueryData} data={speciesQueryData}
title="Species" title="Species"
@@ -33,16 +35,17 @@ export const TaxonomyTab = () => {
useObject={true} useObject={true}
tableRowAction={handleRowActionClick} tableRowAction={handleRowActionClick}
editingAction={(selectedSpecies: Species) => { editingAction={(selectedSpecies: Species) => {
const fixedSpecies = fixNullValuesInTaxonomyFields(selectedSpecies)
setEditData({ setEditData({
...editData, ...editData,
subclass_or_superorder_name: selectedSpecies.subclass_or_superorder_name ?? '', subclass_or_superorder_name: fixedSpecies.subclass_or_superorder_name,
order_name: selectedSpecies.order_name ?? '', order_name: fixedSpecies.order_name!,
suborder_or_superfamily_name: selectedSpecies.suborder_or_superfamily_name ?? '', suborder_or_superfamily_name: fixedSpecies.suborder_or_superfamily_name,
family_name: selectedSpecies.family_name ?? '', family_name: fixedSpecies.family_name!,
subfamily_name: selectedSpecies.subfamily_name ?? '', subfamily_name: fixedSpecies.subfamily_name,
genus_name: selectedSpecies.genus_name ?? '', genus_name: fixedSpecies.genus_name!,
species_name: selectedSpecies.species_name ?? '', species_name: fixedSpecies.species_name!,
unique_identifier: selectedSpecies.unique_identifier ?? '', unique_identifier: fixedSpecies.unique_identifier!,
}) })
}} }}
/> />
@@ -50,16 +53,6 @@ export const TaxonomyTab = () => {
</Box> </Box>
) )
const taxonStatusOptions = [
emptyOption,
'family attrib of genus uncertain',
'genus attrib of species uncertain',
'informal species',
'species validity uncertain',
'taxonomic validity uncertain',
'NOW synonym',
]
const classification = [ const classification = [
[ [
'Class', 'Class',
+1
View File
@@ -57,6 +57,7 @@ export const api = createApi({
'localities', 'localities',
'species', 'species',
'specieslist', 'specieslist',
'speciessynonyms',
'reference', 'reference',
'references', 'references',
'timeunits', 'timeunits',
+2 -1
View File
@@ -28,7 +28,8 @@ const localitiesApi = api.injectEndpoints({
method: 'PUT', method: 'PUT',
body: { locality }, body: { locality },
}), }),
invalidatesTags: (result, _error, { lid }) => (result ? [{ type: 'locality', id: lid }, 'localities'] : []), invalidatesTags: (result, _error, { lid }) =>
result ? [{ type: 'locality', id: lid }, 'localities', 'specieslist'] : [],
}), }),
deleteLocality: builder.mutation<void, number>({ deleteLocality: builder.mutation<void, number>({
query: id => ({ query: id => ({
+6 -6
View File
@@ -7,18 +7,19 @@ const speciesApi = api.injectEndpoints({
query: () => ({ query: () => ({
url: `/species/all`, url: `/species/all`,
}), }),
providesTags: result => (result ? [{ type: 'specieslist' }] : []), providesTags: result => (result ? [{ type: 'specieslist' }, 'speciessynonyms'] : []),
}), }),
getAllSynonyms: builder.query<SpeciesSynonym[], void>({ getAllSynonyms: builder.query<SpeciesSynonym[], void>({
query: () => ({ query: () => ({
url: `/species/synonyms`, url: `/species/synonyms`,
}), }),
providesTags: result => (result ? ['speciessynonyms'] : []),
}), }),
getSpeciesDetails: builder.query<SpeciesDetailsType, string>({ getSpeciesDetails: builder.query<SpeciesDetailsType, string>({
query: id => ({ query: id => ({
url: `/species/${id}`, url: `/species/${id}`,
}), }),
providesTags: result => (result ? [{ type: 'species', id: result.species_id }] : []), providesTags: result => (result ? [{ type: 'species', id: result.species_id }, 'speciessynonyms'] : []),
}), }),
editSpecies: builder.mutation<SpeciesDetailsType, EditDataType<SpeciesDetailsType>>({ editSpecies: builder.mutation<SpeciesDetailsType, EditDataType<SpeciesDetailsType>>({
query: species => ({ query: species => ({
@@ -27,7 +28,7 @@ const speciesApi = api.injectEndpoints({
body: { species }, body: { species },
}), }),
invalidatesTags: (result, _error, { species_id }) => invalidatesTags: (result, _error, { species_id }) =>
result ? [{ type: 'species', id: species_id }, 'specieslist'] : [], result ? [{ type: 'species', id: species_id }, 'specieslist', 'speciessynonyms'] : [],
}), }),
deleteSpecies: builder.mutation<void, number>({ deleteSpecies: builder.mutation<void, number>({
query: id => ({ query: id => ({
@@ -36,15 +37,14 @@ const speciesApi = api.injectEndpoints({
}), }),
invalidatesTags: (result, _error, species_id) => invalidatesTags: (result, _error, species_id) =>
typeof result !== 'undefined' ? [{ type: 'species', id: species_id }, 'specieslist'] : [], typeof result !== 'undefined' ? [{ type: 'species', id: species_id }, 'specieslist', 'speciessynonyms'] : [],
}), }),
}), }),
}) })
export const { export const {
useGetAllSpeciesQuery, useGetAllSpeciesQuery,
useLazyGetAllSpeciesQuery, useGetAllSynonymsQuery,
useLazyGetAllSynonymsQuery,
useGetSpeciesDetailsQuery, useGetSpeciesDetailsQuery,
useEditSpeciesMutation, useEditSpeciesMutation,
useDeleteSpeciesMutation, useDeleteSpeciesMutation,
+16
View File
@@ -0,0 +1,16 @@
// cannot be imported from DetailView -> misc since files in shared folder cannot import from elsewhere in frontend
const emptyOption = {
optionDisplay: 'No value',
display: '',
value: '',
}
export const taxonStatusOptions = [
emptyOption,
'family attrib of genus uncertain',
'genus attrib of species uncertain',
'informal species',
'species validity uncertain',
'taxonomic validity uncertain',
'NOW synonym',
]
+1
View File
@@ -332,6 +332,7 @@ export type Species = {
locomo2: string | null locomo2: string | null
locomo3: string | null locomo3: string | null
sp_comment: string | null sp_comment: string | null
sp_author: string | null
} }
export type RegionDetails = Prisma.now_reg_coord & { now_reg_coord_people: Array<RegionCoordinator> } & { export type RegionDetails = Prisma.now_reg_coord & { now_reg_coord_people: Array<RegionCoordinator> } & {
+10
View File
@@ -1,3 +1,4 @@
import { taxonStatusOptions } from '../taxonStatusOptions'
import { EditDataType, SpeciesDetailsType } from '../types' import { EditDataType, SpeciesDetailsType } from '../types'
import { Validators, validator } from './validator' import { Validators, validator } from './validator'
@@ -80,6 +81,7 @@ export const validateSpecies = (
name: 'Species', name: 'Species',
required: true, required: true,
asString: (speciesName: string) => { asString: (speciesName: string) => {
if (speciesName.indexOf(' ') !== -1) return 'Species name must not contain any spaces.'
if (speciesName !== 'indet.' && editData.genus_name === 'indet.') if (speciesName !== 'indet.' && editData.genus_name === 'indet.')
return 'when the Genus is indet., Species must also be indet.' return 'when the Genus is indet., Species must also be indet.'
if (speciesName !== 'sp.' && editData.genus_name === 'gen.') if (speciesName !== 'sp.' && editData.genus_name === 'gen.')
@@ -89,6 +91,14 @@ export const validateSpecies = (
return return
}, },
}, },
taxonomic_status: {
name: 'Taxonomic Status',
asString: (taxonomicStatus: string) => {
if (!taxonStatusOptions.slice(1).includes(taxonomicStatus) && taxonomicStatus !== '')
return `Taxonomic Status must be one of the following (or left empty): ${taxonStatusOptions.slice(1).join(', ')}.`
return
},
},
unique_identifier: { unique_identifier: {
name: 'Unique Identifier', name: 'Unique Identifier',
required: true, required: true,
+31
View File
@@ -0,0 +1,31 @@
import { EditDataType, SpeciesSynonym } from '../types'
import { Validators, validator } from './validator'
export const validateSynonym = (
editData: EditDataType<SpeciesSynonym>,
fieldName: keyof EditDataType<SpeciesSynonym>
) => {
const validators: Validators<Partial<EditDataType<SpeciesSynonym>>> = {
syn_genus_name: {
name: 'Synonym genus name',
required: true,
asString: (synGenusName: string) => {
if (synGenusName.indexOf(' ') !== -1) return 'Genus must not contain any spaces.'
return
},
},
syn_species_name: {
name: 'Synonym species name',
required: true,
asString: (synSpeciesName: string) => {
if (synSpeciesName !== 'indet.' && editData.syn_genus_name === 'indet.')
return 'when the Genus is indet., Species must also be indet.'
if (synSpeciesName !== 'sp.' && editData.syn_genus_name === 'gen.')
return 'when the Genus is gen., Species must be sp.'
return
},
},
}
return validator<EditDataType<SpeciesSynonym>>(validators, editData, fieldName)
}
@@ -1,4 +1,4 @@
import { EditDataType, Species, SpeciesSynonym } from '@/shared/types' import { EditDataType, Species, SpeciesDetailsType, SpeciesSynonym } from '@/shared/types'
const generateMultipleParentsError = ( const generateMultipleParentsError = (
invalidField: string, invalidField: string,
@@ -11,7 +11,10 @@ Please contact the NOW administration to fix this taxonomy.`
const isDuplicateTaxon = (newSpecies: EditDataType<Species>, existingSpecies: Species) => { const isDuplicateTaxon = (newSpecies: EditDataType<Species>, existingSpecies: Species) => {
if ( if (
newSpecies.species_id !== existingSpecies.species_id && // the check for undefined species_id is here to make sure you cannot add two new,
// identical species at the same time (e.g. in Locality -> Species -> Add new species)
((newSpecies.species_id === undefined && existingSpecies.species_id === undefined) ||
newSpecies.species_id !== existingSpecies.species_id) &&
newSpecies.genus_name === existingSpecies.genus_name && newSpecies.genus_name === existingSpecies.genus_name &&
newSpecies.species_name === existingSpecies.species_name && newSpecies.species_name === existingSpecies.species_name &&
newSpecies.unique_identifier === existingSpecies.unique_identifier newSpecies.unique_identifier === existingSpecies.unique_identifier
@@ -133,7 +136,7 @@ const checkSubFamilyGenus = (newSpecies: EditDataType<Species>, existingSpecies:
) )
} }
export const checkTaxonomy = ( export const checkSpeciesTaxonomy = (
newSpecies: EditDataType<Species>, newSpecies: EditDataType<Species>,
existingSpeciesArray: Species[], existingSpeciesArray: Species[],
synonyms: SpeciesSynonym[] synonyms: SpeciesSynonym[]
@@ -314,7 +317,7 @@ const convertTaxonomyField = (value: string | null | undefined) => {
return convertedValue return convertedValue
} }
export const convertTaxonomyFields = (species: EditDataType<Species>) => { export const convertSpeciesTaxonomyFields = (species: EditDataType<Species>) => {
const speciesName = species.species_name const speciesName = species.species_name
let lowercasedSpeciesName = '' let lowercasedSpeciesName = ''
if (speciesName) { if (speciesName) {
@@ -336,3 +339,31 @@ export const convertTaxonomyFields = (species: EditDataType<Species>) => {
} }
return convertedSpecies as EditDataType<Species> return convertedSpecies as EditDataType<Species>
} }
export const fixNullValuesInTaxonomyFields = (species: Species | SpeciesDetailsType) => {
return {
...species,
subclass_or_superorder_name: species.subclass_or_superorder_name ?? '',
suborder_or_superfamily_name: species.suborder_or_superfamily_name ?? '',
subfamily_name: species.subfamily_name ?? '',
taxonomic_status: species.taxonomic_status ?? '',
}
}
export const convertSynonymTaxonomyFields = (synonym: EditDataType<SpeciesSynonym>) => {
const synSpeciesName = synonym.syn_species_name
let lowercasedSynSpeciesName = ''
if (synSpeciesName) {
if (synSpeciesName !== 'indet.') {
lowercasedSynSpeciesName = synSpeciesName.toLowerCase()
} else {
lowercasedSynSpeciesName = synSpeciesName
}
}
const convertedSynonym = {
...synonym,
syn_genus_name: convertTaxonomyField(synonym.syn_genus_name),
syn_species_name: lowercasedSynSpeciesName,
}
return convertedSynonym as EditDataType<SpeciesSynonym>
}