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

1700: Refactor and clean up csv import #1765

Open
wants to merge 3 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
6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion administration/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ module.exports = {
},
overrides: [
{
files: ['*.test.{ts,tsx}', '**/__mocks__/*.{ts,tsx}', 'jest.setup.ts', 'jest.config.ts'],
files: ['*.test.{ts,tsx}', '**/__mocks__/*.{ts,tsx}', '**/testing/*.{ts,tsx}', 'jest.setup.ts', 'jest.config.ts'],
rules: {
'global-require': 'off',
'no-console': 'off',
Expand Down
4 changes: 2 additions & 2 deletions administration/src/bp-modules/cards/AddCardsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import styled from 'styled-components'
import { Card, initializeCard, initializeCardFromCSV } from '../../cards/Card'
import { Region } from '../../generated/graphql'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import { getCsvHeaders } from '../../project-configs/helper'
import AddCardForm from './AddCardForm'
import CardFormButton from './CardFormButton'
import { getHeaders } from './ImportCardsController'

const FormsWrapper = styled(FlipMove)`
flex-wrap: wrap;
Expand Down Expand Up @@ -55,7 +55,7 @@ const AddCardsForm = ({

useEffect(() => {
if (cards.length === 0) {
const headers = getHeaders(projectConfig)
const headers = getCsvHeaders(projectConfig)
const values = headers.map(header => searchParams.get(header))
setCards([initializeCardFromCSV(projectConfig.card, values, headers, region, true)])

Expand Down
40 changes: 5 additions & 35 deletions administration/src/bp-modules/cards/ImportCardsController.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,20 @@
import { NonIdealState, Spinner } from '@blueprintjs/core'
import React, { ReactElement, useCallback, useContext, useMemo } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import React, { ReactElement, useContext } from 'react'
import { useNavigate } from 'react-router-dom'

import { FREINET_PARAM } from '../../Router'
import { WhoAmIContext } from '../../WhoAmIProvider'
import { Card, initializeCardFromCSV } from '../../cards/Card'
import { Region } from '../../generated/graphql'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import { ProjectConfig } from '../../project-configs/getProjectConfig'
import useBlockNavigation from '../../util/useBlockNavigation'
import GenerationFinished from './CardsCreatedMessage'
import CreateCardsButtonBar from './CreateCardsButtonBar'
import { convertFreinetImport } from './ImportCardsFromFreinetController'
import ImportCardsInput from './ImportCardsInput'
import CardImportTable from './ImportCardsTable'
import useCardGenerator, { CardActivationState } from './hooks/useCardGenerator'

export const getHeaders = (projectConfig: ProjectConfig): string[] => [
projectConfig.card.nameColumnName,
projectConfig.card.expiryColumnName,
...(projectConfig.card.extensionColumnNames.filter(Boolean) as string[]),
]

const InnerImportCardsController = ({ region }: { region: Region }): ReactElement => {
const { state, setState, generateCardsPdf, generateCardsCsv, setCards, cards } = useCardGenerator(region)
const projectConfig = useContext(ProjectConfigContext)
const headers = useMemo(() => getHeaders(projectConfig), [projectConfig])
const navigate = useNavigate()

const isFreinetFormat = new URLSearchParams(useLocation().search).get(FREINET_PARAM) === 'true'

useBlockNavigation({
when: cards.length > 0,
message: 'Falls Sie fortfahren, werden alle Eingaben verworfen.',
Expand All @@ -43,20 +28,10 @@ const InnerImportCardsController = ({ region }: { region: Region }): ReactElemen
}
}

// TODO headers or csvHeader?
const lineToCard = useCallback(
(line: string[], csvHeader: string[]): Card => {
if (isFreinetFormat) {
convertFreinetImport(line, csvHeader, projectConfig)
}
return initializeCardFromCSV(projectConfig.card, line, csvHeader, region)
},
[projectConfig, region, isFreinetFormat]
)

if (state === CardActivationState.loading) {
return <Spinner />
}

if (state === CardActivationState.finished) {
return (
<GenerationFinished
Expand All @@ -71,14 +46,9 @@ const InnerImportCardsController = ({ region }: { region: Region }): ReactElemen
return (
<>
{cards.length === 0 ? (
<ImportCardsInput
setCards={setCards}
lineToCard={lineToCard}
headers={headers}
isFreinetFormat={isFreinetFormat}
/>
<ImportCardsInput setCards={setCards} region={region} />
) : (
<CardImportTable cards={cards} cardConfig={projectConfig.card} headers={headers} />
<CardImportTable cards={cards} />
)}
<CreateCardsButtonBar
cards={cards}
Expand Down
148 changes: 97 additions & 51 deletions administration/src/bp-modules/cards/ImportCardsInput.test.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import { OverlayToaster } from '@blueprintjs/core'
import { fireEvent, render, waitFor } from '@testing-library/react'
import React, { ReactNode } from 'react'
import { fireEvent, waitFor } from '@testing-library/react'
import React from 'react'

import { Card, initializeCard } from '../../cards/Card'
import { Region } from '../../generated/graphql'
import { ProjectConfigProvider } from '../../project-configs/ProjectConfigContext'
import bayernConfig from '../../project-configs/bayern/config'
import { LOCAL_STORAGE_PROJECT_KEY } from '../../project-configs/constants'
import { ProjectConfig } from '../../project-configs/getProjectConfig'
import { ProjectConfig, setProjectConfigOverride } from '../../project-configs/getProjectConfig'
import koblenzConfig from '../../project-configs/koblenz/config'
import nuernbergConfig from '../../project-configs/nuernberg/config'
import { renderWithRouter } from '../../testing/render'
import PlainDate from '../../util/PlainDate'
import { AppToasterProvider } from '../AppToaster'
import { getHeaders } from './ImportCardsController'
import ImportCardsInput, { ENTRY_LIMIT } from './ImportCardsInput'

jest.mock('../../Router', () => ({}))

const wrapper = ({ children }: { children: ReactNode }) => (
<AppToasterProvider>
<ProjectConfigProvider>{children}</ProjectConfigProvider>
</AppToasterProvider>
)

describe('ImportCardsInput', () => {
beforeEach(jest.clearAllMocks)

const region: Region = {
id: 0,
name: 'augsburg',
Expand All @@ -30,12 +26,10 @@ describe('ImportCardsInput', () => {
activatedForCardConfirmationMail: true,
}

const renderAndSubmitCardsInput = async (
projectConfig: ProjectConfig,
csv: string,
lineToCard: () => Card,
setCards: () => void
) => {
const toaster = jest.spyOn(OverlayToaster.prototype, 'show')
const setCards = jest.fn()

const renderAndSubmitCardsInput = async (projectConfig: ProjectConfig, csv: string, setCards: () => void) => {
const fileReaderMock = {
// eslint-disable-next-line func-names
readAsText: jest.fn(function (this: FileReader, _: Blob) {
Expand All @@ -44,17 +38,14 @@ describe('ImportCardsInput', () => {
} as unknown as FileReader
jest.spyOn(global, 'FileReader').mockReturnValue(fileReaderMock)
const file = new File([csv], `${projectConfig.name}.csv`, { type: 'text/csv' })

localStorage.setItem(LOCAL_STORAGE_PROJECT_KEY, projectConfig.projectId)

const { getByTestId } = render(
<ImportCardsInput
headers={getHeaders(projectConfig)}
lineToCard={lineToCard}
setCards={setCards}
isFreinetFormat={false}
/>,
{ wrapper }
setProjectConfigOverride(projectConfig.projectId)

const { getByTestId } = renderWithRouter(
<AppToasterProvider>
<ProjectConfigProvider>
<ImportCardsInput setCards={setCards} region={region} />
</ProjectConfigProvider>
</AppToasterProvider>
)

const fileInput = getByTestId('file-upload') as HTMLInputElement
Expand All @@ -65,33 +56,90 @@ describe('ImportCardsInput', () => {
await waitFor(() => expect(fileReaderMock.readAsText).toHaveBeenCalledTimes(1))
}

it.each([
{
projectConfig: bayernConfig,
csv: `
it('should correctly import CSV Card for bayern', async () => {
const projectConfig = bayernConfig
const csv = `
Name,Ablaufdatum,Kartentyp
Thea Test,03.04.2024,Standard
Tilo Traber,,Gold
`,
},
{
projectConfig: nuernbergConfig,
csv: `
Name,Ablaufdatum,Geburtsdatum,Passnummer
`
await renderAndSubmitCardsInput(projectConfig, csv, setCards)

expect(toaster).not.toHaveBeenCalled()
expect(setCards).toHaveBeenCalledTimes(1)
expect(setCards).toHaveBeenCalledWith([
{
expirationDate: PlainDate.fromCustomFormat('03.04.2024'),
extensions: { bavariaCardType: 'Standard', regionId: 0 },
fullName: 'Thea Test',
id: expect.any(Number),
},
{ expirationDate: null, extensions: { regionId: 0 }, fullName: 'Tilo Traber', 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
`,
},
])(`Correctly import CSV Card for project $projectConfig.name`, async ({ projectConfig, csv }) => {
const toaster = jest.spyOn(OverlayToaster.prototype, 'show')
const lineToCard = jest.fn(() => initializeCard(projectConfig.card, region))
const setCards = jest.fn()
`

await renderAndSubmitCardsInput(projectConfig, csv, setCards)

expect(toaster).not.toHaveBeenCalled()
expect(setCards).toHaveBeenCalledTimes(1)
expect(setCards).toHaveBeenCalledWith([
{
expirationDate: PlainDate.fromCustomFormat('03.04.2024'),
extensions: { birthday: PlainDate.fromCustomFormat('10.10.2000'), regionId: 0, nuernbergPassId: 12345678 },
fullName: 'Thea Test',
id: expect.any(Number),
},
{
expirationDate: PlainDate.fromCustomFormat('03.04.2025'),
extensions: { birthday: PlainDate.fromCustomFormat('12.01.1984'), regionId: 0, nuernbergPassId: 98765432 },
fullName: 'Tilo Traber',
id: expect.any(Number),
},
])
})

it('should correctly import CSV Card for koblenz', async () => {
const projectConfig = koblenzConfig
const csv = `
Name,Ablaufdatum,Geburtsdatum,Referenznummer
Thea Test,03.04.2024,10.10.2000,123k
Tilo Traber,03.04.2025,12.01.1984,98765432
`

await renderAndSubmitCardsInput(projectConfig, csv, lineToCard, setCards)
await renderAndSubmitCardsInput(projectConfig, csv, setCards)

expect(toaster).not.toHaveBeenCalled()
expect(setCards).toHaveBeenCalledTimes(1)
expect(lineToCard).toHaveBeenCalledTimes(2)
expect(setCards).toHaveBeenCalledWith([
{
expirationDate: PlainDate.fromCustomFormat('03.04.2024'),
extensions: {
birthday: PlainDate.fromCustomFormat('10.10.2000'),
regionId: 0,
koblenzReferenceNumber: '123k',
},
fullName: 'Thea Test',
id: expect.any(Number),
},
{
expirationDate: PlainDate.fromCustomFormat('03.04.2025'),
extensions: {
birthday: PlainDate.fromCustomFormat('12.01.1984'),
regionId: 0,
koblenzReferenceNumber: '98765432',
},
fullName: 'Tilo Traber',
id: expect.any(Number),
},
])
})

it.each([
Expand All @@ -118,15 +166,13 @@ ${'Thea Test,03.04.2024,12345678\n'.repeat(ENTRY_LIMIT + 1)}
`,
error: `Die Datei hat mehr als ${ENTRY_LIMIT} Einträge.`,
},
])(`Import CSV Card should fail with error '$error'`, async ({ csv, error }) => {
])(`import CSV Card should fail with error '$error'`, async ({ csv, error }) => {
const toaster = jest.spyOn(OverlayToaster.prototype, 'show')
const lineToCard = jest.fn(() => initializeCard(bayernConfig.card, region))
const setCards = jest.fn()

await renderAndSubmitCardsInput(bayernConfig, csv, lineToCard, setCards)
await renderAndSubmitCardsInput(bayernConfig, csv, setCards)

expect(toaster).toHaveBeenCalledWith({ intent: 'danger', message: error })
expect(setCards).not.toHaveBeenCalled()
expect(lineToCard).not.toHaveBeenCalled()
})
})
Loading