Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1868: Check mandatory extensions #1891

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 128 additions & 8 deletions administration/src/bp-modules/cards/ImportCardsInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { OverlayToaster } from '@blueprintjs/core'
import { fireEvent, waitFor } from '@testing-library/react'
import React from 'react'

import { BAVARIA_CARD_TYPE_GOLD, BAVARIA_CARD_TYPE_STANDARD } from '../../cards/extensions/BavariaCardTypeExtension'
import { Region } from '../../generated/graphql'
import { ProjectConfigProvider } from '../../project-configs/ProjectConfigContext'
import bayernConfig from '../../project-configs/bayern/config'
Expand Down Expand Up @@ -61,7 +62,7 @@ describe('ImportCardsInput', () => {
const csv = `
Name,Ablaufdatum,Kartentyp
Thea Test,03.04.2024,Standard
Tilo Traber,,Gold
Tilo Traber,,gold
`
await renderAndSubmitCardsInput(projectConfig, csv, setCards)

Expand All @@ -70,20 +71,54 @@ Tilo Traber,,Gold
expect(setCards).toHaveBeenCalledWith([
{
expirationDate: PlainDate.fromCustomFormat('03.04.2024'),
extensions: { bavariaCardType: 'Standard', regionId: 0 },
extensions: { bavariaCardType: BAVARIA_CARD_TYPE_STANDARD, regionId: 0 },
fullName: 'Thea Test',
id: expect.any(Number),
},
{ expirationDate: null, extensions: { regionId: 0 }, fullName: 'Tilo Traber', id: expect.any(Number) },
{
expirationDate: null,
extensions: { regionId: 0, bavariaCardType: BAVARIA_CARD_TYPE_GOLD },
fullName: 'Tilo Traber',
id: expect.any(Number),
},
])
})

it('should correctly import CSV Card for bayern freinet', async () => {
jest.spyOn(URLSearchParams.prototype, 'get').mockReturnValue('true')

const projectConfig = bayernConfig
const csv = `
inhaber_ehrenamtskarte;eak_datum;eak_karten_status;co_name;anrede;titel;vorname;nachname;strasse;plz;ort
�Blau�;01.12.2029;Karte abgelaufen;;Herr;;Maxim;Musterin;Kirchgasse 30;97346;Iphofen
�Gold�;;Karte an EA verschickt;;Herr;;Max;Muster;Kleinlangheimer Stra�e 12;97355;Kleinlangheim
`
await renderAndSubmitCardsInput(projectConfig, csv, setCards)

expect(toaster).not.toHaveBeenCalled()
expect(setCards).toHaveBeenCalledTimes(1)
expect(setCards).toHaveBeenCalledWith([
{
expirationDate: PlainDate.fromCustomFormat('01.12.2029'),
extensions: { bavariaCardType: BAVARIA_CARD_TYPE_STANDARD, regionId: 0 },
fullName: 'Maxim Musterin',
id: expect.any(Number),
},
{
expirationDate: null,
extensions: { regionId: 0, bavariaCardType: BAVARIA_CARD_TYPE_GOLD },
fullName: 'Max Muster',
id: expect.any(Number),
},
])
})

it('should correctly import CSV Card for nuernberg', async () => {
const projectConfig = nuernbergConfig
const csv = `
Name,Ablaufdatum,Geburtsdatum,Pass-ID
Thea Test,03.04.2024,10.10.2000,12345678
Tilo Traber,03.04.2025,12.01.1984,98765432
Name,Ablaufdatum,Startdatum,Geburtsdatum,Pass-ID
Thea Test,03.04.2024,01.01.2026,10.10.2000,12345678
Tilo Traber,03.04.2025,01.01.2026,12.01.1984,98765432
`

await renderAndSubmitCardsInput(projectConfig, csv, setCards)
Expand All @@ -93,13 +128,23 @@ Tilo Traber,03.04.2025,12.01.1984,98765432
expect(setCards).toHaveBeenCalledWith([
{
expirationDate: PlainDate.fromCustomFormat('03.04.2024'),
extensions: { birthday: PlainDate.fromCustomFormat('10.10.2000'), regionId: 0, nuernbergPassId: 12345678 },
extensions: {
birthday: PlainDate.fromCustomFormat('10.10.2000'),
regionId: 0,
nuernbergPassId: 12345678,
startDay: PlainDate.fromCustomFormat('01.01.2026'),
},
fullName: 'Thea Test',
id: expect.any(Number),
},
{
expirationDate: PlainDate.fromCustomFormat('03.04.2025'),
extensions: { birthday: PlainDate.fromCustomFormat('12.01.1984'), regionId: 0, nuernbergPassId: 98765432 },
extensions: {
birthday: PlainDate.fromCustomFormat('12.01.1984'),
regionId: 0,
nuernbergPassId: 98765432,
startDay: PlainDate.fromCustomFormat('01.01.2026'),
},
fullName: 'Tilo Traber',
id: expect.any(Number),
},
Expand Down Expand Up @@ -175,4 +220,79 @@ ${'Thea Test,03.04.2024,12345678\n'.repeat(ENTRY_LIMIT + 1)}
expect(toaster).toHaveBeenCalledWith({ intent: 'danger', message: error })
expect(setCards).not.toHaveBeenCalled()
})

it.each([
{
csv: `
Name,Ablaufdatum
Thea Test,03.04.2024
Tilo Traber,
`,
error: 'Die Datei verfügt nicht über das gültige Spaltenformat. Es fehlen notwendige Spalten.',
project: bayernConfig,
missingColumn: 'Kartentyp',
},
{
csv: `
Name,Ablaufdatum,Geburtsdatum,Pass-ID
Thea Test,03.04.2024,10.10.2000,12345678
Tilo Traber,03.04.2025,12.01.1984,98765432
`,
error: 'Die Datei verfügt nicht über das gültige Spaltenformat. Es fehlen notwendige Spalten.',
project: nuernbergConfig,
missingColumn: 'Startdatum',
},
{
csv: `
Name,Ablaufdatum,Startdatum,Geburtsdatum
Thea Test,03.04.2024,01.01.2026,10.10.2000
Tilo Traber,03.04.2025,01.01.2026,12.01.1984
`,
error: 'Die Datei verfügt nicht über das gültige Spaltenformat. Es fehlen notwendige Spalten.',
project: nuernbergConfig,
missingColumn: 'Pass-ID',
},
{
csv: `
Name,Ablaufdatum,Startdatum,Pass-ID
Thea Test,03.04.2024,01.01.2026,12345678
Tilo Traber,03.04.2025,01.01.2026,98765432
`,
error: 'Die Datei verfügt nicht über das gültige Spaltenformat. Es fehlen notwendige Spalten.',
project: nuernbergConfig,
missingColumn: 'Geburtsdatum',
},

{
csv: `
Name,Ablaufdatum,Geburtsdatum
Thea Test,03.04.2024,10.10.2000
Tilo Traber,03.04.2025,12.01.1984
`,
error: 'Die Datei verfügt nicht über das gültige Spaltenformat. Es fehlen notwendige Spalten.',
project: koblenzConfig,
missingColumn: 'Referenznummer',
},
{
csv: `
Name,Ablaufdatum,Referenznummer
Thea Test,03.04.2024,123k
Tilo Traber,03.04.2025,98765432
`,
error: 'Die Datei verfügt nicht über das gültige Spaltenformat. Es fehlen notwendige Spalten.',
project: koblenzConfig,
missingColumn: 'Geburtsdatum',
},
])(
`import CSV Card should fail if '$missingColumn' is not provided for '$project.name'`,
async ({ csv, error, project }) => {
const toaster = jest.spyOn(OverlayToaster.prototype, 'show')
const setCards = jest.fn()

await renderAndSubmitCardsInput(project, csv, setCards)

expect(toaster).toHaveBeenCalledWith({ intent: 'danger', message: error })
expect(setCards).not.toHaveBeenCalled()
}
)
})
10 changes: 9 additions & 1 deletion administration/src/bp-modules/cards/ImportCardsInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import styled from 'styled-components'

import { FREINET_PARAM } from '../../Router'
import { Card, initializeCardFromCSV } from '../../cards/Card'
import { Card, cardHasAllMandatoryExtensions, initializeCardFromCSV } from '../../cards/Card'
import { Region } from '../../generated/graphql'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import { getCsvHeaders } from '../../project-configs/helper'
Expand Down Expand Up @@ -92,6 +92,14 @@

const [csvHeaders, ...entries] = isFreinetFormat ? convertFreinetImport(lines, projectConfig) : lines
const cards = entries.map(line => initializeCardFromCSV(projectConfig.card, line, csvHeaders, region))
const cardsHaveAllMandatoryExtensions = cards.every(card =>
cardHasAllMandatoryExtensions(card, projectConfig.card)
)

if (!cardsHaveAllMandatoryExtensions) {
showInputError(t('importFileMissingData'))
return
}

Check warning on line 102 in administration/src/bp-modules/cards/ImportCardsInput.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ Getting worse: Complex Method

ImportCardsInput increases in cyclomatic complexity from 13 to 14, threshold = 10. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

setCards(cards)
setInputState('idle')
Expand Down
5 changes: 5 additions & 0 deletions administration/src/cards/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ export const isExpirationDateValid = (card: Card, { nullable } = { nullable: fal
)
}

export const cardHasAllMandatoryExtensions = (card: Card, cardConfig: CardConfig): boolean => {
const mandatoryExtensions = cardConfig.extensions.filter(extension => extension.isMandatory)
return mandatoryExtensions.every(extension => Object.keys(card.extensions).includes(extension.name))
}

export const isValid = (card: Card, { expirationDateNullable } = { expirationDateNullable: false }): boolean =>
isFullNameValid(card) &&
getExtensions(card).every(({ extension, state }) => extension.isValid(state)) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const getAddressFieldExtension = <T extends AddressFieldExtension>(
toString: (state): string => state[name],
fromSerialized: (value: string) => ({ [name]: value } as AddressFieldExtensionState<T>),
serialize: (state): string => state[name],
isMandatory: false,
})

export const AddressLine1Extension = getAddressFieldExtension(ADDRESS_LINE_1_EXTENSION)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const BavariaCardTypeExtension: Extension<BavariaCardTypeExtensionState> = {
toString,
fromSerialized: fromString,
serialize: toString,
isMandatory: true,
}

export default BavariaCardTypeExtension
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const BirthdayExtension: Extension<BirthdayExtensionState> = {
return birthday === null ? null : { birthday }
},
serialize: ({ birthday }: BirthdayExtensionState) => birthday?.formatISO() ?? '',
isMandatory: true,
}

export default BirthdayExtension
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const EMailNotificationExtension: Extension<EmailNotificationExtensionState> = {
toString,
fromSerialized: fromString,
serialize: toString,
isMandatory: false,
}

export default EMailNotificationExtension
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const KoblenzReferenceNumberExtension: Extension<KoblenzReferenceNumberExtension
toString,
fromSerialized: fromString,
serialize: toString,
isMandatory: true,
}

export default KoblenzReferenceNumberExtension
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const NuernbergPassIdExtension: Extension<NuernbergPassIdExtensionState> = {
toString,
fromSerialized: fromString,
serialize: toString,
isMandatory: true,
}

export default NuernbergPassIdExtension
1 change: 1 addition & 0 deletions administration/src/cards/extensions/RegionExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const RegionExtension: Extension<RegionExtensionState> = {
fromString,
fromSerialized: fromString,
serialize: toString,
isMandatory: true,
}

export default RegionExtension
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const StartDayExtension: Extension<StartDayExtensionState> = {
return startDay === null ? null : { startDay }
},
serialize: ({ startDay }: StartDayExtensionState) => startDay.formatISO(),
isMandatory: true,
}

export default StartDayExtension
1 change: 1 addition & 0 deletions administration/src/cards/extensions/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type Extension<T = Record<string, unknown>> = {
toString(state: T): string
fromSerialized(value: string): T | null
serialize(state: T): string
isMandatory: boolean
}

const Extensions = [
Expand Down
1 change: 1 addition & 0 deletions administration/src/util/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
"importFileTooManyEntries": "Die Datei hat mehr als {{limit}} Einträge.",
"importFileWrongFormat": "Die gewählte Datei hat einen unzulässigen Dateityp.",
"importFileTooBig": "Die ausgewählte Datei ist zu groß.",
"importFileMissingData": "Die Datei verfügt nicht über das gültige Spaltenformat. Es fehlen notwendige Spalten.",
"printQRCodes": "QR-Codes drucken",
"validationError": "Validierungsfehler"
},
Expand Down