English. Spicy jalapeno bacon ipsum
',
round_seats: '',
},
- testEmployees: {},
+ courseCode: 'SF1624',
}
render(
@@ -195,15 +192,15 @@ describe('Component ', () => {
test('renders course offering number of places correctly and default text in modal if selection criteria is empty', async () => {
const propsWithEmptyCriteria = {
- showRoundData: true,
- courseHasRound: true,
+ memoStorageURI: '',
+ semesterRoundState: defaultSemesterRoundState,
courseData: {},
courseRound: {
round_course_term: ['2018', '1'],
round_selection_criteria: '',
round_seats: '5-10',
},
- testEmployees: {},
+ courseCode: 'SF1624',
}
render(
diff --git a/public/js/app/components/statistics/CheckboxOption.jsx b/public/js/app/components/statistics/CheckboxOption.jsx
index 385e91f1..9fb5aa45 100644
--- a/public/js/app/components/statistics/CheckboxOption.jsx
+++ b/public/js/app/components/statistics/CheckboxOption.jsx
@@ -39,7 +39,7 @@ const optionsReducer = (state, action) => {
function CheckboxOption({ paramName, onChange, stateMode }) {
/* depends on type of document to dropdown */
- const [context] = useWebContext()
+ const context = useWebContext()
const [{ options }, setOptions] = React.useReducer(optionsReducer, { options: context[paramName] || [] }) // ???
const {
diff --git a/public/js/app/components/statistics/DropdownOption.jsx b/public/js/app/components/statistics/DropdownOption.jsx
index 6aed6bdf..6832d3fa 100644
--- a/public/js/app/components/statistics/DropdownOption.jsx
+++ b/public/js/app/components/statistics/DropdownOption.jsx
@@ -8,7 +8,7 @@ import { getOptionsValues } from './domain/formConfigurations'
import { frameIfEmpty } from './domain/validation'
function DropdownOption({ paramName, onChange, showInfoBox = true }) {
- const [context] = useWebContext()
+ const context = useWebContext()
const [option, setOption] = React.useState(context[paramName])
const {
diff --git a/public/js/app/components/statistics/MemosSummary.jsx b/public/js/app/components/statistics/MemosSummary.jsx
index 957f5823..acd16acc 100644
--- a/public/js/app/components/statistics/MemosSummary.jsx
+++ b/public/js/app/components/statistics/MemosSummary.jsx
@@ -116,7 +116,7 @@ function MemosNumbersCharts({ statisticsResult }) {
]
const { combinedMemosPerSchool: docsPerSchool, periods, year, school } = statisticsResult
const { schools = {} } = docsPerSchool
- const [{ languageIndex }] = useWebContext()
+ const { languageIndex } = useWebContext()
if (school === 'allSchools') {
schools.allSchools = addAllSchoolsData(docsPerSchool)
}
diff --git a/public/js/app/components/statistics/RadioboxOption.jsx b/public/js/app/components/statistics/RadioboxOption.jsx
index dcb4d4a9..168522d1 100644
--- a/public/js/app/components/statistics/RadioboxOption.jsx
+++ b/public/js/app/components/statistics/RadioboxOption.jsx
@@ -8,7 +8,7 @@ import { frameIfEmpty } from './domain/validation'
import { getOptionsValues, splitToBulks } from './domain/formConfigurations'
function RadioboxOption({ paramName, onChange }) {
- const [context] = useWebContext()
+ const context = useWebContext()
const [option, setOption] = React.useState(context[paramName])
const {
translation: {
diff --git a/public/js/app/components/statistics/StatisticsDataTable.jsx b/public/js/app/components/statistics/StatisticsDataTable.jsx
index 8c4bddaf..a88c7acd 100644
--- a/public/js/app/components/statistics/StatisticsDataTable.jsx
+++ b/public/js/app/components/statistics/StatisticsDataTable.jsx
@@ -288,7 +288,7 @@ function FilterTable({ onFilter, placeholder, searchLabel }) {
function StatisticsDataTable({ statisticsResult }) {
const [resetPaginationToggle, setResetPaginationToggle] = useState(false)
- const [context] = useWebContext()
+ const context = useWebContext()
const { browserConfig } = context
const {
translation: { statisticsLabels },
diff --git a/public/js/app/components/statistics/TableSummaryRows.jsx b/public/js/app/components/statistics/TableSummaryRows.jsx
index da145ac1..6e7841e3 100644
--- a/public/js/app/components/statistics/TableSummaryRows.jsx
+++ b/public/js/app/components/statistics/TableSummaryRows.jsx
@@ -46,7 +46,7 @@ function TableFooterRow({ cellNames, cellsContent }) {
}
function TableSummary({ cellNames = [], docsPerSchool = {}, getNumbersFn = () => [], labels = {}, totalNumbers = [] }) {
- const [{ languageIndex }] = useWebContext()
+ const { languageIndex } = useWebContext()
const { schools = {} } = docsPerSchool
diff --git a/public/js/app/context/WebContext.jsx b/public/js/app/context/WebContext.jsx
index 611d6973..1ad9fa52 100644
--- a/public/js/app/context/WebContext.jsx
+++ b/public/js/app/context/WebContext.jsx
@@ -1,5 +1,4 @@
import React from 'react'
-import { addClientFunctionsToWebContext } from '../client-context/addClientFunctionsToWebContext'
const WebContext = React.createContext()
@@ -27,9 +26,7 @@ export const WebContextProvider = props => {
}
}
- // OBS! deviation from NODE-WEB to make functions working
- const [currentConfig, setConfig] = React.useState({ ...config, ...addClientFunctionsToWebContext() })
- const value = [currentConfig, setConfig]
+ const value = { ...config }
// eslint-disable-next-line react/jsx-props-no-spreading
return
}
diff --git a/public/js/app/hooks/__tests__/useApi.test.js b/public/js/app/hooks/__tests__/useApi.test.js
new file mode 100644
index 00000000..13f6cfcd
--- /dev/null
+++ b/public/js/app/hooks/__tests__/useApi.test.js
@@ -0,0 +1,219 @@
+const { renderHook, waitFor } = require('@testing-library/react')
+const { useApi } = require('../useApi')
+const { useWebContext } = require('../../context/WebContext')
+const { useLanguage } = require('../useLanguage')
+const { STATUS } = require('../api/status')
+
+jest.mock('../../context/WebContext')
+jest.mock('../useLanguage')
+
+const mockContext = {
+ paths: {
+ api: {
+ plannedSchemaModules: {
+ uri: '/:courseCode/:semester/:applicationCode',
+ },
+ },
+ },
+}
+
+const apiToCall = jest.fn()
+
+const defaultParams = { courseCode: 'SF1624', semester: 20241, applicationCode: 12345 }
+
+describe('useApi', () => {
+ beforeAll(() => {
+ useWebContext.mockReturnValue(mockContext)
+ apiToCall.mockResolvedValue({
+ status: STATUS.OK,
+ data: 'somePlannedModules',
+ })
+ useLanguage.mockReturnValue({
+ isEnglish: true,
+ languageIndex: 0,
+ })
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ test('calls apiToCall with given parameters', async () => {
+ const { result } = renderHook(() => useApi(apiToCall, { one: 'one', two: 2 }, null, null))
+
+ expect(apiToCall).toHaveBeenLastCalledWith({ one: 'one', two: 2 })
+
+ await waitFor(() => expect(result.current.data).toStrictEqual('somePlannedModules'))
+
+ const anotherApiToCall = jest.fn().mockResolvedValue({
+ status: STATUS.OK,
+ data: 'someData',
+ })
+
+ const { result: result2 } = renderHook(() => useApi(anotherApiToCall, { two: 'two', three: 3 }, null, null))
+
+ expect(anotherApiToCall).toHaveBeenLastCalledWith({ two: 'two', three: 3 })
+
+ await waitFor(() => expect(result2.current.data).toStrictEqual('someData'))
+ })
+
+ test('returns data from apiToCall', async () => {
+ const { result } = renderHook(() => useApi(apiToCall, {}, null, null))
+
+ await waitFor(() => expect(result.current.data).toStrictEqual('somePlannedModules'))
+ })
+
+ test.each(['someDefaultValue', null])('on parameter change sets data to defaultValue: %s', async defaultValue => {
+ jest.useFakeTimers()
+
+ apiToCall.mockImplementationOnce(
+ () =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ resolve({
+ status: STATUS.OK,
+ data: 'someNewPlannedModules',
+ })
+ }, 100)
+ })
+ )
+
+ apiToCall.mockImplementationOnce(
+ () =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ resolve({
+ status: STATUS.OK,
+ data: 'someOtherPlannedModules',
+ })
+ }, 100)
+ })
+ )
+
+ const { result } = renderHook(() => useApi(apiToCall, defaultParams, defaultValue, null))
+
+ expect(result.current.data).toStrictEqual(defaultValue)
+ expect(result.current.isError).toStrictEqual(false)
+
+ jest.advanceTimersByTime(100)
+ expect(apiToCall).toHaveBeenCalledTimes(1)
+
+ await waitFor(() => expect(result.current.data).toStrictEqual('someNewPlannedModules'))
+ await waitFor(() => expect(result.current.isError).toStrictEqual(false))
+
+ result.current.setApiParams({ other: 'params' })
+
+ await waitFor(() => expect(result.current.data).toStrictEqual(defaultValue))
+ await waitFor(() => expect(result.current.isError).toStrictEqual(false))
+
+ jest.advanceTimersByTime(100)
+
+ expect(apiToCall).toHaveBeenCalledTimes(2)
+
+ await waitFor(() => expect(result.current.data).toStrictEqual('someOtherPlannedModules'))
+ await waitFor(() => expect(result.current.isError).toStrictEqual(false))
+
+ jest.useRealTimers()
+ })
+
+ test('if all goes well, isError should be false', async () => {
+ const { result } = renderHook(() => useApi(apiToCall, defaultParams, null, null))
+ await waitFor(() => expect(result.current.isError).toStrictEqual(false))
+ })
+
+ test.each(['', undefined, null])(
+ 'if all goes well, but data is falsy: %s, should return defaulValueIfNullResponse',
+ async data => {
+ apiToCall.mockResolvedValueOnce({
+ status: STATUS.OK,
+ data,
+ })
+ const { result } = renderHook(() => useApi(apiToCall, defaultParams, null, 'defaulValueIfNullResponse'))
+
+ await waitFor(() => expect(result.current.data).toStrictEqual('defaulValueIfNullResponse'))
+ }
+ )
+
+ test.each(['', undefined, null])(
+ 'if all goes well, but data is falsy: %s, should return defaulValueIfNullResponse',
+ async data => {
+ apiToCall.mockResolvedValueOnce({
+ status: STATUS.OK,
+ data,
+ })
+ const { result } = renderHook(() => useApi(apiToCall, defaultParams, null, 'defaulValueIfNullResponseOther'))
+
+ await waitFor(() => expect(result.current.data).toStrictEqual('defaulValueIfNullResponseOther'))
+ }
+ )
+
+ describe('if apiToCall returns status: ERROR', () => {
+ test('isError is true', async () => {
+ apiToCall.mockResolvedValueOnce({
+ status: STATUS.ERROR,
+ data: null,
+ })
+
+ const { result } = renderHook(() => useApi(apiToCall, defaultParams, null, null))
+
+ await waitFor(() => expect(result.current.isError).toStrictEqual(true))
+ })
+
+ test('isError should be reset to false in between calls', async () => {
+ jest.useFakeTimers()
+
+ apiToCall.mockImplementationOnce(
+ () =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ resolve({
+ status: STATUS.ERROR,
+ data: null,
+ })
+ }, 100)
+ })
+ )
+
+ apiToCall.mockImplementationOnce(
+ () =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ resolve({
+ status: STATUS.ERROR,
+ data: null,
+ })
+ }, 100)
+ })
+ )
+
+ const { result } = renderHook(() => useApi(apiToCall, defaultParams, null, null))
+
+ jest.advanceTimersByTime(100)
+
+ await waitFor(() => expect(result.current.isError).toStrictEqual(true))
+
+ result.current.setApiParams({ ...defaultParams, applicationCode: 54321 })
+
+ await waitFor(() => expect(result.current.isError).toStrictEqual(false))
+ jest.advanceTimersByTime(100)
+
+ await waitFor(() => expect(result.current.isError).toStrictEqual(true))
+
+ jest.useRealTimers()
+ })
+
+ test.each(['defaulValueIfNullResponse', 'defaulValueIfNullResponseOther'])(
+ 'plannedModules is set to defaulValueIfNullResponse',
+ async defaulValueIfNullResponse => {
+ apiToCall.mockResolvedValueOnce({
+ status: STATUS.ERROR,
+ data: null,
+ })
+
+ const { result } = renderHook(() => useApi(apiToCall, defaultParams, null, defaulValueIfNullResponse))
+
+ await waitFor(() => expect(result.current.data).toStrictEqual(defaulValueIfNullResponse))
+ }
+ )
+ })
+})
diff --git a/public/js/app/hooks/__tests__/usePlannedModules.test.js b/public/js/app/hooks/__tests__/usePlannedModules.test.js
index a6bbff5f..27154330 100644
--- a/public/js/app/hooks/__tests__/usePlannedModules.test.js
+++ b/public/js/app/hooks/__tests__/usePlannedModules.test.js
@@ -1,9 +1,10 @@
const { renderHook, waitFor } = require('@testing-library/react')
const { usePlannedModules } = require('../usePlannedModules')
-const { getPlannedModules, STATUS } = require('../api/getPlannedModules')
+const { getPlannedModules } = require('../api/getPlannedModules')
const { useWebContext } = require('../../context/WebContext')
const { useLanguage } = require('../useLanguage')
const { INFORM_IF_IMPORTANT_INFO_IS_MISSING } = require('../../util/constants')
+const { STATUS } = require('../api/status')
jest.mock('../api/getPlannedModules')
jest.mock('../../context/WebContext')
@@ -21,14 +22,13 @@ const mockContext = {
const baseParams = { courseCode: 'SF1624', semester: 20241, applicationCode: 12345 }
const defaultParams = { ...baseParams, showRoundData: true }
-const baseParamsWithPath = { ...baseParams, basePath: mockContext.paths.api.plannedSchemaModules.uri }
describe('usePlannedModules', () => {
- beforeAll(() => {
- useWebContext.mockReturnValue([mockContext])
+ beforeEach(() => {
+ useWebContext.mockReturnValue(mockContext)
getPlannedModules.mockResolvedValue({
status: STATUS.OK,
- plannedModules: 'somePlannedModules',
+ data: 'somePlannedModules',
})
useLanguage.mockReturnValue({
isEnglish: true,
@@ -40,55 +40,23 @@ describe('usePlannedModules', () => {
jest.clearAllMocks()
})
- test.each([undefined, null, ''])(
- 'if courseCode is "%s", should return INFORM_IF_IMPORTANT_INFO_IS_MISSING',
- invalidInput => {
- const { result } = renderHook(() => usePlannedModules({ ...defaultParams, courseCode: invalidInput }))
-
- expect(getPlannedModules).not.toHaveBeenCalled()
-
- expect(result.current.plannedModules).toStrictEqual(INFORM_IF_IMPORTANT_INFO_IS_MISSING[0])
- }
- )
-
- test.each([undefined, null, ''])(
- 'if semester is "%s", should return INFORM_IF_IMPORTANT_INFO_IS_MISSING',
- invalidInput => {
- const { result } = renderHook(() => usePlannedModules({ ...defaultParams, semester: invalidInput }))
-
- expect(getPlannedModules).not.toHaveBeenCalled()
-
- expect(result.current.plannedModules).toStrictEqual(INFORM_IF_IMPORTANT_INFO_IS_MISSING[0])
- }
- )
-
- test.each([undefined, null, ''])(
- 'if applicationCode is "%s", should return INFORM_IF_IMPORTANT_INFO_IS_MISSING',
- invalidInput => {
- const { result } = renderHook(() => usePlannedModules({ ...defaultParams, applicationCode: invalidInput }))
+ test('if data is empty string, should return INFORM_IF_IMPORTANT_INFO_IS_MISSING', async () => {
+ getPlannedModules.mockResolvedValue({
+ status: STATUS.OK,
+ data: '',
+ })
- expect(getPlannedModules).not.toHaveBeenCalled()
+ const { result } = renderHook(() => usePlannedModules(defaultParams))
- expect(result.current.plannedModules).toStrictEqual(INFORM_IF_IMPORTANT_INFO_IS_MISSING[0])
- }
- )
+ await waitFor(() => expect(result.current.plannedModules).toStrictEqual(INFORM_IF_IMPORTANT_INFO_IS_MISSING[0]))
+ })
test('if showRoundData is not true, should return null', () => {
const { result } = renderHook(() => usePlannedModules({ ...defaultParams, showRoundData: false }))
- expect(getPlannedModules).not.toHaveBeenCalled()
-
expect(result.current.plannedModules).toStrictEqual(null)
})
- test('calls getPlannedModules with correct parameters', async () => {
- const { result } = renderHook(() => usePlannedModules(defaultParams))
- expect(getPlannedModules).toHaveBeenCalledWith(baseParamsWithPath)
-
- // We have to await a state change to `plannedModules`, because testing-library/react will complain about it otherwise
- await waitFor(() => expect(result.current.plannedModules).toStrictEqual('somePlannedModules'))
- })
-
test('returns plannedModules from getPlannedModules', async () => {
const { result } = renderHook(() => usePlannedModules(defaultParams))
@@ -100,21 +68,17 @@ describe('usePlannedModules', () => {
expect(result.current.plannedModules).toStrictEqual(null)
- expect(getPlannedModules).toHaveBeenCalledTimes(1)
-
await waitFor(() => expect(result.current.plannedModules).toStrictEqual('somePlannedModules'))
getPlannedModules.mockResolvedValueOnce({
status: STATUS.OK,
- plannedModules: 'someOtherPlannedModules',
+ data: 'someOtherPlannedModules',
})
rerender({ params: { ...defaultParams, applicationCode: 54321 } })
await waitFor(() => expect(result.current.plannedModules).toStrictEqual(null))
- expect(getPlannedModules).toHaveBeenCalledTimes(2)
-
await waitFor(() => expect(result.current.plannedModules).toStrictEqual('someOtherPlannedModules'))
})
@@ -124,9 +88,9 @@ describe('usePlannedModules', () => {
})
test('if all goes well, but plannedModules is an empty string, should return INFORM_IF_IMPORTANT_INFO_IS_MISSING', async () => {
- getPlannedModules.mockResolvedValueOnce({
+ getPlannedModules.mockResolvedValue({
status: STATUS.OK,
- plannedModules: '',
+ data: '',
})
const { result } = renderHook(() => usePlannedModules(defaultParams))
@@ -134,20 +98,16 @@ describe('usePlannedModules', () => {
})
test('if all goes well, but plannedModules is an empty string, should return INFORM_IF_IMPORTANT_INFO_IS_MISSING also in swedish', async () => {
- getPlannedModules.mockResolvedValueOnce({
+ getPlannedModules.mockResolvedValue({
status: STATUS.OK,
- plannedModules: '',
+ data: '',
})
- // Apparently useLanguage is called twice here, so we have to mock the non-default value twice.
- useLanguage.mockReturnValueOnce({
- isEnglish: false,
- languageIndex: 1,
- })
- useLanguage.mockReturnValueOnce({
+ useLanguage.mockReturnValue({
isEnglish: false,
languageIndex: 1,
})
+
const { result } = renderHook(() => usePlannedModules(defaultParams))
await waitFor(() => expect(result.current.plannedModules).toStrictEqual(INFORM_IF_IMPORTANT_INFO_IS_MISSING[1]))
@@ -155,9 +115,9 @@ describe('usePlannedModules', () => {
describe('if getPlannedModules returns status: ERROR', () => {
test('isError is true', async () => {
- getPlannedModules.mockResolvedValueOnce({
+ getPlannedModules.mockResolvedValue({
status: STATUS.ERROR,
- plannedModules: null,
+ data: null,
})
const { result } = renderHook(() => usePlannedModules(defaultParams))
@@ -165,32 +125,10 @@ describe('usePlannedModules', () => {
await waitFor(() => expect(result.current.isError).toStrictEqual(true))
})
- test('isError should be reset to false in between calls', async () => {
- getPlannedModules.mockResolvedValueOnce({
- status: STATUS.ERROR,
- plannedModules: null,
- })
-
- const { result, rerender } = renderHook(({ params = defaultParams } = {}) => usePlannedModules(params))
-
- await waitFor(() => expect(result.current.isError).toStrictEqual(true))
-
- getPlannedModules.mockResolvedValueOnce({
- status: STATUS.ERROR,
- plannedModules: null,
- })
-
- rerender({ params: { ...defaultParams, applicationCode: 54321 } })
-
- await waitFor(() => expect(result.current.isError).toStrictEqual(false))
-
- await waitFor(() => expect(result.current.isError).toStrictEqual(true))
- })
-
test('plannedModules is set to INFORM_IF_IMPORTANT_INFO_IS_MISSING', async () => {
- getPlannedModules.mockResolvedValueOnce({
+ getPlannedModules.mockResolvedValue({
status: STATUS.ERROR,
- plannedModules: null,
+ data: null,
})
const { result } = renderHook(() => usePlannedModules(defaultParams))
@@ -199,25 +137,18 @@ describe('usePlannedModules', () => {
})
test('plannedModules is set to INFORM_IF_IMPORTANT_INFO_IS_MISSING also in swedish', async () => {
- getPlannedModules.mockResolvedValueOnce({
+ getPlannedModules.mockResolvedValue({
status: STATUS.ERROR,
- plannedModules: null,
+ data: null,
})
- // Apparently useLanguage is called twice here, so we have to mock the non-default value twice.
- useLanguage.mockReturnValueOnce({
- isEnglish: false,
- languageIndex: 1,
- })
- useLanguage.mockReturnValueOnce({
+ useLanguage.mockReturnValue({
isEnglish: false,
languageIndex: 1,
})
const { result } = renderHook(() => usePlannedModules(defaultParams))
- expect(getPlannedModules).toHaveBeenCalledTimes(1)
-
await waitFor(() => expect(result.current.plannedModules).toStrictEqual(INFORM_IF_IMPORTANT_INFO_IS_MISSING[1]))
})
})
diff --git a/public/js/app/hooks/__tests__/useSemesterRoundState.test.js b/public/js/app/hooks/__tests__/useSemesterRoundState.test.js
new file mode 100644
index 00000000..c919478f
--- /dev/null
+++ b/public/js/app/hooks/__tests__/useSemesterRoundState.test.js
@@ -0,0 +1,441 @@
+const roundsBySemester = {
+ 20222: [
+ {
+ round_time_slots: 'No information inserted',
+ round_start_date: '30 Oct 2022',
+ round_end_date: '15 Jan 2023',
+ round_target_group: 'No information inserted',
+ round_tutoring_form: 'NML',
+ round_tutoring_time: 'DAG',
+ round_tutoring_language: 'Swedish',
+ round_course_place: 'KTH Campus',
+ round_campus: 'KTH Campus',
+ round_short_name: 'CMATD1 m.fl.',
+ round_application_code: '52226',
+ round_schedule: 'No information inserted',
+ round_study_pace: 50,
+ round_course_term: ['2022', '2'],
+ round_periods: 'P2 (7.5 hp)',
+ round_seats: '',
+ round_selection_criteria: '',
+ round_type: 'Programutbildning',
+ round_funding_type: 'ORD',
+ round_application_link: 'No information inserted',
+ round_part_of_programme:
+ '\n \n Degree Programme in Civil Engineering and Urban Management, åk 1, Mandatory\n \n
\n \n Degree Programme in Engineering Chemistry, åk 1, Mandatory\n \n
\n \n Degree Programme in Materials Design and Engineering, åk 1, Mandatory\n \n
\n \n Master of Science in Engineering and in Education, åk 2, MAKE, Mandatory\n \n
\n \n Master of Science in Engineering and in Education, åk 2, TEDA, Mandatory\n \n
',
+ round_state: 'APPROVED',
+ round_comment: '',
+ round_category: 'PU',
+ },
+ ],
+ 20232: [
+ {
+ round_time_slots: 'No information inserted',
+ round_start_date: '30 Oct 2023',
+ round_end_date: '15 Jan 2024',
+ round_target_group: 'No information inserted',
+ round_tutoring_form: 'NML',
+ round_tutoring_time: 'DAG',
+ round_tutoring_language: 'Swedish',
+ round_course_place: 'KTH Campus',
+ round_campus: 'KTH Campus',
+ round_short_name: 'CMATD1 m.fl.',
+ round_application_code: '51446',
+ round_schedule: 'No information inserted',
+ round_study_pace: 50,
+ round_course_term: ['2023', '2'],
+ round_periods: 'P2 (7.5 hp)',
+ round_seats: '',
+ round_selection_criteria: '',
+ round_type: 'Programutbildning',
+ round_funding_type: 'ORD',
+ round_application_link: 'No information inserted',
+ round_part_of_programme:
+ '\n \n Degree Programme in Civil Engineering and Urban Management, åk 1, Mandatory\n \n
\n \n Degree Programme in Engineering Chemistry, åk 1, Mandatory\n \n
\n \n Degree Programme in Materials Design and Engineering, åk 1, Mandatory\n \n
\n \n Master of Science in Engineering and in Education, åk 2, MAKE, Mandatory\n \n
\n \n Master of Science in Engineering and in Education, åk 2, TEDA, Mandatory\n \n
',
+ round_state: 'APPROVED',
+ round_comment: '',
+ round_category: 'PU',
+ },
+ {
+ round_time_slots: 'No information inserted',
+ round_start_date: '30 Oct 2023',
+ round_end_date: '15 Jan 2024',
+ round_target_group: 'No information inserted',
+ round_tutoring_form: 'NML',
+ round_tutoring_time: 'DAG',
+ round_tutoring_language: 'Swedish',
+ round_course_place: 'KTH Campus',
+ round_campus: 'KTH Campus',
+ round_short_name: 'CDATE1',
+ round_application_code: '51350',
+ round_schedule: 'No information inserted',
+ round_study_pace: 50,
+ round_course_term: ['2023', '2'],
+ round_periods: 'P2 (7.5 hp)',
+ round_seats: '',
+ round_selection_criteria: '',
+ round_type: 'Programutbildning',
+ round_funding_type: 'ORD',
+ round_application_link: 'No information inserted',
+ round_part_of_programme:
+ '\n \n Degree Programme in Computer Science and Engineering, åk 1, Mandatory\n \n
',
+ round_state: 'APPROVED',
+ round_comment: '',
+ round_category: 'PU',
+ },
+ ],
+ 20242: [
+ {
+ round_time_slots: 'No information inserted',
+ round_start_date: '28 Oct 2024',
+ round_end_date: '13 Jan 2025',
+ round_target_group: 'No information inserted',
+ round_tutoring_form: 'NML',
+ round_tutoring_time: 'DAG',
+ round_tutoring_language: 'Swedish',
+ round_course_place: 'KTH Campus',
+ round_campus: 'KTH Campus',
+ round_short_name: 'CMETE1 m.fl.',
+ round_application_code: '50233',
+ round_schedule: 'No information inserted',
+ round_study_pace: 50,
+ round_course_term: ['2024', '2'],
+ round_periods: 'P2 (7.5 hp)',
+ round_seats: '',
+ round_selection_criteria: '',
+ round_type: 'Programutbildning',
+ round_funding_type: 'ORD',
+ round_application_link: 'No information inserted',
+ round_part_of_programme:
+ '\n \n Degree Programme Open Entrance, åk 1, Mandatory\n \n
\n \n Degree Programme in Media Technology, åk 1, Mandatory\n \n
',
+ round_state: 'APPROVED',
+ round_comment: '',
+ round_category: 'PU',
+ },
+ {
+ round_time_slots: 'No information inserted',
+ round_start_date: '28 Oct 2024',
+ round_end_date: '13 Jan 2025',
+ round_target_group: 'No information inserted',
+ round_tutoring_form: 'NML',
+ round_tutoring_time: 'DAG',
+ round_tutoring_language: 'Swedish',
+ round_course_place: 'KTH Kista',
+ round_campus: 'KTH Kista',
+ round_short_name: 'CINTE1',
+ round_application_code: '51098',
+ round_schedule: 'No information inserted',
+ round_study_pace: 50,
+ round_course_term: ['2024', '2'],
+ round_periods: 'P2 (7.5 hp)',
+ round_seats: '',
+ round_selection_criteria: '',
+ round_type: 'Programutbildning',
+ round_funding_type: 'ORD',
+ round_application_link: 'No information inserted',
+ round_part_of_programme:
+ '\n \n Degree Programme in Information and Communication Technology, åk 1, Mandatory\n \n
',
+ round_state: 'APPROVED',
+ round_comment: '',
+ round_category: 'PU',
+ },
+ ],
+}
+
+const syllabusList = [
+ {
+ course_goals:
+ 'After the course the student should be able to
- use concepts. theorems and methods to solve and present solutions to problems within the parts of linear algebra described by the course content,
- read and comprehend mathematical text.
',
+ course_content:
+ "Vectors, matrices, linear equations, Gaussian elimination, vector geometry with dot product and vector product, determinants, vector spaces, linear independence, bases, change of basis, linear transformations, the least-squares method, eigenvalues, eigenvectors, quadratic forms, orthogonality, inner-product space, Gram-Schmidt's method.
",
+ course_eligibility: 'Basic requirements.
',
+ course_requirments_for_final_grade: 'Written exam, possibly with the possibility of continuous examination.
',
+ course_literature: 'No information inserted',
+ course_literature_comment:
+ 'Announced no later than 4 weeks before the start of the course on the course web page.
',
+ course_valid_from: {
+ year: 2019,
+ semesterNumber: 2,
+ },
+ course_valid_to: [],
+ course_required_equipment: 'No information inserted',
+ course_examination:
+ "- TEN1 - \n Examination,\n 7.5 credits, \n grading scale: A, B, C, D, E, FX, F \n
",
+ course_examination_comments:
+ 'Based on recommendation from KTH’s coordinator for disabilities, the examiner will decide how to adapt an examination for students with documented disability.
The examiner may apply another examination format when re-examining individual students.The examiner decides, in consultation with KTHs Coordinator of students with disabilities (Funka), about any customized examination for students with documented, lasting disability.
',
+ course_ethical:
+ "- All members of a group are responsible for the group's work.
- In any assessment, every student shall honestly disclose any help received and sources used.
- In an oral assessment, every student shall be able to present and answer questions about the entire assignment and solution.
",
+ course_additional_regulations: '',
+ course_transitional_reg: '',
+ course_decision_to_discontinue: 'No information inserted',
+ },
+ {
+ course_goals:
+ 'After completing the course students should for a passing grade be able to
- use the basic concepts and problem solving methods in linear algebra and geometry. In particular it means to be able to:
- understand, interpret and use the basic concepts: the vector space Rn, subspaces of Rn, linear dependence and independence, basis, dimension, linear transformations, matrix, determinant, eigenvalue and eigenvector.
- solve geometric problems in two and three dimensions using for example vectors, dot product, vector product, triple product and projection.
- use Gauss-Jordan?s method for example to solve linear systems of equations, calculate inverse matrices, determinants and to resolve questions about linearly independent.
- use matrix and determinant calculus to address issues regarding linear transformations and linear systems.
- use the least-squares method to solve for example problems with over-determined linear systems of equations.
- use different bases for vector spaces to handle vectors and linear transformations, and to manage changes of bases and linear coordinate transformations.
- compute eigenvalues and eigenvectors and use this for example in order to diagonalize matrices, to study quadratic forms, conics in the plane and quadratic surfaces in three space.
- use the Euclidean inner product in order to address the questions
about distance, orthogonality and projection, and apply Gram-Schmidt?s
method to calculate orthogonal bases of subspaces. - set up simple mathematical models where the fundamental concepts in linear algebra and geometry are used, discuss the relevance of such
models, reasonableness and accuracy, and know how mathematical software can be used for calculations and visualization. - read and understand mathematical texts about for example, vectors, matrices, linear transformations and their applications, communicate mathematical reasoning and calculations in this area, orally and in writing in such a way that they are easy to follow.
For higher grades, the student in addition should be able to:
- manage general vector spaces, such as function spaces or vector spaces of matrices.
- use other inner products than the Euclidean inner product.
- derive important relations in linear algebra and geometry.
- generalize and adapt the methods to use in somewhat new contexts.
- solve problems that require synthesis of material and ideas from all over the course.
- describe the theory behind concepts such as eigenvalues and orthogonality.
',
+ course_content:
+ 'Vectors, matrices, linear equations, Gaussian elimination, vector geometry with dot product and vector product, determinants, vector spaces, linear independence, bases, change of basis, the least-squares method, eigenvalues, eigenvectors, quadratic forms, orthogonality, inner-product space, Gram-Schmidt?s method
',
+ course_eligibility:
+ 'Basic and specific requirements for engineering program.
Mandatory for first year, can not be read by other students
',
+ course_requirments_for_final_grade: 'Written exam, possibly with the possibility of continuous examination.
',
+ course_literature: 'No information inserted',
+ course_literature_comment: 'No information inserted',
+ course_valid_from: {
+ year: 2010,
+ semesterNumber: 2,
+ },
+ course_valid_to: {
+ year: 2019,
+ semesterNumber: 1,
+ },
+ course_required_equipment: 'No information inserted',
+ course_examination:
+ "- TEN1 - \n Examination,\n 7.5 credits, \n grading scale: A, B, C, D, E, FX, F \n
",
+ course_examination_comments:
+ 'Based on recommendation from KTH’s coordinator for disabilities, the examiner will decide how to adapt an examination for students with documented disability.
The examiner may apply another examination format when re-examining individual students.',
+ course_ethical:
+ "- All members of a group are responsible for the group's work.
- In any assessment, every student shall honestly disclose any help received and sources used.
- In an oral assessment, every student shall be able to present and answer questions about the entire assignment and solution.
",
+ course_additional_regulations: '',
+ course_transitional_reg: '',
+ course_decision_to_discontinue: 'No information inserted',
+ },
+]
+
+const activeSemesters = [
+ {
+ year: '2023',
+ semesterNumber: '2',
+ semester: '20232',
+ },
+ {
+ year: '2024',
+ semesterNumber: '2',
+ semester: '20242',
+ },
+]
+
+const { renderHook, waitFor, act } = require('@testing-library/react')
+const { useSemesterRoundState } = require('../useSemesterRoundState')
+
+describe('useSemesterRoundsLogic', () => {
+ test('returns correct values', () => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: 20242,
+ roundsBySemester,
+ syllabusList,
+ activeSemesters,
+ })
+ )
+
+ expect(result.current.selectedRoundIndex).toBe(undefined)
+ expect(result.current.activeRound).toEqual({})
+ expect(result.current.selectedSemester).toBe(20242)
+ expect(result.current.showRoundData).toBe(false)
+ expect(result.current.activeSemesterOnlyHasOneRound).toBe(false)
+ expect(result.current.firstRoundInActiveSemester).toEqual(roundsBySemester[20242][0])
+ expect(result.current.hasActiveSemesters).toBe(true)
+ expect(result.current.activeSyllabus).toBe(syllabusList[0])
+ expect(result.current.hasSyllabus).toBe(true)
+ })
+
+ test('updates selectedRoundIndex, showRoundData and activeRound when setSelectedRoundIndex is called', async () => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: 20242,
+ roundsBySemester,
+ syllabusList,
+ activeSemesters,
+ })
+ )
+ expect(result.current.selectedRoundIndex).toBe(undefined)
+ expect(result.current.activeRound).toEqual({})
+ expect(result.current.showRoundData).toBe(false)
+
+ act(() => {
+ result.current.setSelectedRoundIndex(0)
+ })
+
+ await waitFor(() => expect(result.current.selectedRoundIndex).toBe(0))
+ await waitFor(() => expect(result.current.showRoundData).toBe(true))
+ await waitFor(() => expect(result.current.activeRound).toEqual(roundsBySemester[20242][0]))
+ })
+
+ test('updates selectedSemester, firstRoundInActiveSemester, when setSelectedSemester is called', async () => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: 20242,
+ roundsBySemester,
+ syllabusList,
+ activeSemesters,
+ })
+ )
+
+ act(() => {
+ result.current.setSelectedSemester(20232)
+ })
+
+ await waitFor(() => expect(result.current.selectedSemester).toBe(20232))
+ await waitFor(() => expect(result.current.firstRoundInActiveSemester).toBe(roundsBySemester[20232][0]))
+ })
+
+ test('if selectedRoundIndex is set and setSelectedSemester is called, reset selectedRoundIndex, activeRound and showRoundData', async () => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: 20242,
+ roundsBySemester,
+ syllabusList,
+ activeSemesters,
+ })
+ )
+ expect(result.current.selectedRoundIndex).toBe(undefined)
+ expect(result.current.showRoundData).toBe(false)
+ expect(result.current.activeRound).toEqual({})
+
+ act(() => {
+ result.current.setSelectedRoundIndex(0)
+ })
+
+ await waitFor(() => expect(result.current.selectedRoundIndex).toBe(0))
+ await waitFor(() => expect(result.current.showRoundData).toBe(true))
+ await waitFor(() => expect(result.current.activeRound).toEqual(roundsBySemester[20242][0]))
+
+ act(() => {
+ result.current.setSelectedSemester(20232)
+ })
+ await waitFor(() => expect(result.current.selectedRoundIndex).toBe(undefined))
+ await waitFor(() => expect(result.current.showRoundData).toBe(false))
+ await waitFor(() => expect(result.current.activeRound).toEqual({}))
+ })
+
+ test('if a semester with only one round is selected, that round should be selected', async () => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: 20222,
+ roundsBySemester,
+ syllabusList,
+ activeSemesters,
+ })
+ )
+ expect(result.current.activeSemesterOnlyHasOneRound).toBe(true)
+ expect(result.current.showRoundData).toBe(true)
+
+ expect(result.current.activeRound).toEqual(roundsBySemester[20222][0])
+ })
+
+ test('if initiallySelectedSemester is a string, converts it to number', async () => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: '20222',
+ roundsBySemester,
+ syllabusList,
+ activeSemesters,
+ })
+ )
+
+ expect(result.current.selectedSemester).toStrictEqual(20222)
+ })
+
+ test.each([[], undefined])(
+ 'if roundsBySemester is empty or undefined, returns values accordingly',
+ async invalidRoundsBySemester => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: 20242,
+ roundsBySemester: invalidRoundsBySemester,
+ syllabusList,
+ activeSemesters,
+ })
+ )
+
+ expect(result.current.activeSemesterOnlyHasOneRound).toBe(false)
+ expect(result.current.showRoundData).toBe(false)
+ expect(result.current.firstRoundInActiveSemester).toEqual({})
+
+ expect(result.current.selectedRoundIndex).toBe(undefined)
+ expect(result.current.activeRound).toEqual({})
+ }
+ )
+
+ test('picks correct syllabus for selected semester', async () => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: 20182,
+ roundsBySemester,
+ syllabusList,
+ activeSemesters,
+ })
+ )
+
+ expect(result.current.activeSyllabus).toBe(syllabusList[1])
+ expect(result.current.hasSyllabus).toBe(true)
+ })
+
+ test('if no syllabuses are, sets activeSyllabus to undefined and hasSyllabus to false', async () => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: 20182,
+ roundsBySemester,
+ syllabusList: [],
+ activeSemesters,
+ })
+ )
+
+ expect(result.current.activeSyllabus).toEqual(undefined)
+ expect(result.current.hasSyllabus).toBe(false)
+ })
+
+ test('if no valid syllabus is given, sets activeSyllabus to undefined and hasSyllabus to false', async () => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: 20182,
+ roundsBySemester,
+ syllabusList: [{ course_valid_from: [], course_valid_to: [] }],
+ activeSemesters,
+ })
+ )
+
+ expect(result.current.activeSyllabus).toEqual(undefined)
+ expect(result.current.hasSyllabus).toBe(false)
+ })
+
+ test('calling resetSelectedRoundIndex resets round values', async () => {
+ const { result } = renderHook(() =>
+ useSemesterRoundState({
+ initiallySelectedRoundIndex: undefined,
+ initiallySelectedSemester: 20242,
+ roundsBySemester,
+ syllabusList,
+ activeSemesters,
+ })
+ )
+
+ expect(result.current.selectedRoundIndex).toBe(undefined)
+ expect(result.current.showRoundData).toBe(false)
+ expect(result.current.activeRound).toEqual({})
+
+ act(() => {
+ result.current.setSelectedRoundIndex(0)
+ })
+
+ await waitFor(() => expect(result.current.selectedRoundIndex).toBe(0))
+ await waitFor(() => expect(result.current.showRoundData).toBe(true))
+ await waitFor(() => expect(result.current.activeRound).toEqual(roundsBySemester[20242][0]))
+
+ act(() => {
+ result.current.resetSelectedRoundIndex()
+ })
+ await waitFor(() => expect(result.current.selectedRoundIndex).toBe(undefined))
+ await waitFor(() => expect(result.current.showRoundData).toBe(false))
+ await waitFor(() => expect(result.current.activeRound).toEqual({}))
+ })
+})
diff --git a/public/js/app/hooks/api/getCourseEmployees.js b/public/js/app/hooks/api/getCourseEmployees.js
new file mode 100644
index 00000000..93378ca4
--- /dev/null
+++ b/public/js/app/hooks/api/getCourseEmployees.js
@@ -0,0 +1,45 @@
+import { STATUS } from './status'
+
+export const getCourseEmployees = async ({ uri, courseCode, selectedSemester, applicationCode }) => {
+ if (!uri || !courseCode || !selectedSemester || !applicationCode) {
+ return {
+ status: STATUS.OK,
+ data: null,
+ }
+ }
+
+ const data = {
+ courseCode,
+ semester: selectedSemester,
+ applicationCodes: [applicationCode],
+ }
+
+ try {
+ const result = await fetch(uri, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ })
+
+ if (!result.ok) {
+ return {
+ status: STATUS.ERROR,
+ data: null,
+ }
+ }
+
+ const courseRoundEmployees = await result.json()
+
+ return {
+ status: STATUS.OK,
+ data: courseRoundEmployees,
+ }
+ } catch (error) {
+ return {
+ status: STATUS.ERROR,
+ data: null,
+ }
+ }
+}
diff --git a/public/js/app/hooks/api/getPlannedModules.js b/public/js/app/hooks/api/getPlannedModules.js
index ad6d4da8..927e640e 100644
--- a/public/js/app/hooks/api/getPlannedModules.js
+++ b/public/js/app/hooks/api/getPlannedModules.js
@@ -1,9 +1,13 @@
-export const STATUS = {
- OK: 'OK',
- ERROR: 'ERROR',
-}
+import { STATUS } from './status'
export const getPlannedModules = async ({ basePath, courseCode, semester, applicationCode }) => {
+ if (!basePath || !courseCode || !semester || !applicationCode) {
+ return {
+ status: STATUS.OK,
+ data: null,
+ }
+ }
+
try {
const uri = basePath
.replace(':courseCode', courseCode)
@@ -15,7 +19,7 @@ export const getPlannedModules = async ({ basePath, courseCode, semester, applic
if (!result.ok) {
return {
status: STATUS.ERROR,
- plannedModules: null,
+ data: null,
}
}
@@ -23,12 +27,12 @@ export const getPlannedModules = async ({ basePath, courseCode, semester, applic
return {
status: STATUS.OK,
- plannedModules,
+ data: plannedModules,
}
} catch (error) {
return {
status: STATUS.ERROR,
- plannedModules: null,
+ data: null,
}
}
}
diff --git a/public/js/app/hooks/api/status.js b/public/js/app/hooks/api/status.js
new file mode 100644
index 00000000..31262b24
--- /dev/null
+++ b/public/js/app/hooks/api/status.js
@@ -0,0 +1,4 @@
+export const STATUS = {
+ OK: 'OK',
+ ERROR: 'ERROR',
+}
diff --git a/public/js/app/hooks/getValidSyllabusForSemester.js b/public/js/app/hooks/getValidSyllabusForSemester.js
new file mode 100644
index 00000000..5d4b4f0d
--- /dev/null
+++ b/public/js/app/hooks/getValidSyllabusForSemester.js
@@ -0,0 +1,14 @@
+const { convertYearSemesterNumberIntoSemester } = require('../../../../server/util/semesterUtils')
+
+const isSyllabusValidForThisSemester = (syllabusStartSemester, semester) => syllabusStartSemester <= semester
+
+const getValidSyllabusForSemester = (publicSyllabusVersions, semester) =>
+ publicSyllabusVersions.find(syllabus => {
+ const prevSyllabusStartSemester = convertYearSemesterNumberIntoSemester(syllabus.course_valid_from)
+
+ return isSyllabusValidForThisSemester(prevSyllabusStartSemester, semester)
+ })
+
+module.exports = {
+ getValidSyllabusForSemester,
+}
diff --git a/public/js/app/hooks/statisticsUseAsync.js b/public/js/app/hooks/statisticsUseAsync.js
index f6a855d4..46d5fb8a 100644
--- a/public/js/app/hooks/statisticsUseAsync.js
+++ b/public/js/app/hooks/statisticsUseAsync.js
@@ -95,6 +95,7 @@ function renderAlertToTop(error = {}, languageIndex) {
}
function dismountTopAlert() {
const alertContainer = document.getElementById('alert-placeholder')
+ // TODO React complains that this should use the containerRoot from above
const alertContainerRoot = createRoot(alertContainer)
if (alertContainer) alertContainerRoot.unmount()
}
@@ -104,7 +105,7 @@ function _getThisHost(thisHostBaseUrl) {
}
function useStatisticsAsync(chosenOptions, loadType = 'onChange') {
- const [{ proxyPrefixPath }] = useWebContext()
+ const { proxyPrefixPath } = useWebContext()
const { languageShortname, languageIndex } = useLanguage()
const { documentType } = chosenOptions
const dependenciesList = loadType === 'onChange' ? [chosenOptions] : []
diff --git a/public/js/app/hooks/useApi.js b/public/js/app/hooks/useApi.js
new file mode 100644
index 00000000..f8b70421
--- /dev/null
+++ b/public/js/app/hooks/useApi.js
@@ -0,0 +1,32 @@
+import { useEffect, useState } from 'react'
+import { STATUS } from './api/status'
+
+export const useApi = (apiToCall, initialApiParams, defaultValue, defaulValueIfNullResponse) => {
+ const [apiParams, setApiParams] = useState(initialApiParams)
+ const [data, setData] = useState(defaultValue)
+ const [isError, setIsError] = useState(false)
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setData(defaultValue)
+ setIsError(false)
+
+ const result = await apiToCall(apiParams)
+
+ setData(result.data || defaulValueIfNullResponse)
+ setIsError(result.status === STATUS.ERROR)
+ }
+
+ fetchData()
+
+ // we do not want to react on defaultValue and defaulValueIfNullResponse, because otherwise
+ // we cannot use empty objects
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [apiToCall, apiParams])
+
+ return {
+ data,
+ isError,
+ setApiParams,
+ }
+}
diff --git a/public/js/app/hooks/useCourseEmployees.js b/public/js/app/hooks/useCourseEmployees.js
new file mode 100644
index 00000000..0b0a0df8
--- /dev/null
+++ b/public/js/app/hooks/useCourseEmployees.js
@@ -0,0 +1,31 @@
+import { useEffect } from 'react'
+import { useWebContext } from '../context/WebContext'
+import { useApi } from './useApi'
+import { getCourseEmployees } from './api/getCourseEmployees'
+
+export const useCourseEmployees = ({ courseCode, selectedSemester, applicationCode }) => {
+ const context = useWebContext()
+
+ const { uri } = context.paths.api.employees
+
+ const { data, isError, setApiParams } = useApi(
+ getCourseEmployees,
+ {
+ uri,
+ courseCode,
+ selectedSemester,
+ applicationCode,
+ },
+ {},
+ {}
+ )
+
+ useEffect(() => {
+ setApiParams({ uri, courseCode, selectedSemester, applicationCode })
+ }, [applicationCode, courseCode, selectedSemester, setApiParams, uri])
+
+ return {
+ courseRoundEmployees: data,
+ isError,
+ }
+}
diff --git a/public/js/app/hooks/useLanguage.js b/public/js/app/hooks/useLanguage.js
index 51bc1ef7..13f4c4d9 100644
--- a/public/js/app/hooks/useLanguage.js
+++ b/public/js/app/hooks/useLanguage.js
@@ -5,7 +5,7 @@ import i18n from '../../../../i18n'
import { useWebContext } from '../context/WebContext'
export const useLanguage = () => {
- const [{ lang }] = useWebContext()
+ const { lang } = useWebContext()
const translation = React.useMemo(() => i18n.getLanguageByShortname(lang), [lang])
diff --git a/public/js/app/hooks/usePlannedModules.js b/public/js/app/hooks/usePlannedModules.js
index 9802cc92..a737a7ac 100644
--- a/public/js/app/hooks/usePlannedModules.js
+++ b/public/js/app/hooks/usePlannedModules.js
@@ -1,40 +1,36 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useMemo } from 'react'
import { useWebContext } from '../context/WebContext'
-import { getPlannedModules, STATUS } from './api/getPlannedModules'
+import { getPlannedModules } from './api/getPlannedModules'
+import { useApi } from './useApi'
import { useMissingInfo } from './useMissingInfo'
const MISSING_INFO = ''
export const usePlannedModules = ({ courseCode, semester, applicationCode, showRoundData }) => {
- const [context] = useWebContext()
+ const context = useWebContext()
const { missingInfoLabel } = useMissingInfo()
- const [plannedModules, setPlannedModules] = useState(null)
- const [isError, setIsError] = useState(false)
+ const basePath = context.paths.api.plannedSchemaModules.uri
- useEffect(() => {
- const fetchData = async () => {
- setPlannedModules(null)
- setIsError(false)
+ const { data, isError, setApiParams } = useApi(
+ getPlannedModules,
+ { basePath, courseCode, semester, applicationCode },
+ null,
+ MISSING_INFO
+ )
- if (!showRoundData) {
- return
- }
+ useEffect(() => {
+ setApiParams({ basePath, courseCode, semester, applicationCode })
+ }, [applicationCode, basePath, courseCode, semester, setApiParams])
- if (!courseCode || !semester || !applicationCode) {
- setPlannedModules(MISSING_INFO)
- } else {
- const basePath = context.paths.api.plannedSchemaModules.uri
- const result = await getPlannedModules({ basePath, courseCode, semester, applicationCode })
- setPlannedModules(result.plannedModules || MISSING_INFO)
- setIsError(result.status === STATUS.ERROR)
- }
- }
- fetchData()
- }, [applicationCode, context, courseCode, semester, showRoundData])
+ const plannedModules = useMemo(() => {
+ if (!showRoundData) return null
+ const pla = data === MISSING_INFO ? missingInfoLabel : data
+ return pla
+ }, [data, missingInfoLabel, showRoundData])
return {
- plannedModules: plannedModules === MISSING_INFO ? missingInfoLabel : plannedModules,
+ plannedModules,
isError,
}
}
diff --git a/public/js/app/hooks/useRoundUtils.js b/public/js/app/hooks/useRoundUtils.js
index 9faa3c13..9a1dc43e 100644
--- a/public/js/app/hooks/useRoundUtils.js
+++ b/public/js/app/hooks/useRoundUtils.js
@@ -30,7 +30,7 @@ export const useRoundUtils = () => {
return `${semesterStringOrEmpty}${roundYear} ${roundLabel}`
},
- [createRoundLabel]
+ [createRoundLabel, translation.courseInformation.course_short_semester]
)
return {
diff --git a/public/js/app/hooks/useSemesterRoundState.js b/public/js/app/hooks/useSemesterRoundState.js
new file mode 100644
index 00000000..037bb2e9
--- /dev/null
+++ b/public/js/app/hooks/useSemesterRoundState.js
@@ -0,0 +1,107 @@
+const { useState, useMemo, useCallback } = require('react')
+const { getValidSyllabusForSemester } = require('./getValidSyllabusForSemester')
+
+const UNSET_VALUE = undefined
+
+const getElementOrEmpty = (arr, index) => {
+ if (!arr || arr.length === 0 || index === undefined || index >= arr.length) {
+ return {}
+ }
+ return arr[index]
+}
+
+const useSemesterRoundState = ({
+ initiallySelectedRoundIndex,
+ initiallySelectedSemester,
+ roundsBySemester,
+ syllabusList,
+ activeSemesters,
+}) => {
+ const [selectedRoundIndex, setSelectedRoundIndex] = useState(initiallySelectedRoundIndex)
+ const [selectedSemester, setSelectedSemester] = useState(Number(initiallySelectedSemester))
+
+ const isSetSelectedRoundIndex = useMemo(() => selectedRoundIndex !== UNSET_VALUE, [selectedRoundIndex])
+
+ const resetSelectedRoundIndex = useCallback(() => setSelectedRoundIndex(() => UNSET_VALUE), [setSelectedRoundIndex])
+
+ const determineSemesterOnlyHasOneRound = (rounds, semester) =>
+ rounds !== undefined &&
+ Object.hasOwnProperty.call(rounds, semester) &&
+ rounds[semester] &&
+ rounds[semester].length === 1
+
+ const activeSemesterOnlyHasOneRound = useMemo(
+ () => determineSemesterOnlyHasOneRound(roundsBySemester, selectedSemester),
+ [roundsBySemester, selectedSemester]
+ )
+
+ const showRoundData = useMemo(() => {
+ if (isSetSelectedRoundIndex || activeSemesterOnlyHasOneRound) {
+ return true
+ }
+
+ return false
+ }, [isSetSelectedRoundIndex, activeSemesterOnlyHasOneRound])
+
+ const roundsForActiveSemester = useMemo(() => {
+ if (!roundsBySemester) {
+ return []
+ }
+
+ return roundsBySemester[selectedSemester]
+ }, [roundsBySemester, selectedSemester])
+
+ const firstRoundInActiveSemester = useMemo(
+ () => getElementOrEmpty(roundsForActiveSemester, 0),
+ [roundsForActiveSemester]
+ )
+
+ const activeRound = useMemo(() => {
+ const index = activeSemesterOnlyHasOneRound ? 0 : selectedRoundIndex
+
+ return getElementOrEmpty(roundsForActiveSemester, index)
+ }, [activeSemesterOnlyHasOneRound, roundsForActiveSemester, selectedRoundIndex])
+
+ const hasActiveSemesters = useMemo(() => activeSemesters && activeSemesters.length > 0, [activeSemesters])
+
+ const activeSyllabus = useMemo(
+ () => getValidSyllabusForSemester(syllabusList, selectedSemester),
+ [syllabusList, selectedSemester]
+ )
+
+ const hasSyllabus = useMemo(
+ () => syllabusList && syllabusList.length > 0 && activeSyllabus !== undefined,
+ [activeSyllabus, syllabusList]
+ )
+
+ const setSelectedSemesterAsNumber = useCallback(
+ newActiveSemester => {
+ setSelectedSemester(() => Number(newActiveSemester))
+ if (determineSemesterOnlyHasOneRound(roundsBySemester, selectedSemester)) {
+ setSelectedRoundIndex(0)
+ } else {
+ resetSelectedRoundIndex()
+ }
+ },
+ [resetSelectedRoundIndex, roundsBySemester, selectedSemester]
+ )
+
+ return {
+ selectedRoundIndex,
+ activeRound,
+ selectedSemester,
+ showRoundData,
+ activeSemesterOnlyHasOneRound,
+ firstRoundInActiveSemester,
+ hasActiveSemesters,
+ activeSyllabus,
+ setSelectedRoundIndex,
+ resetSelectedRoundIndex,
+ setSelectedSemester: setSelectedSemesterAsNumber,
+ hasSyllabus,
+ }
+}
+
+module.exports = {
+ useSemesterRoundState,
+}
diff --git a/public/js/app/pages/CoursePage.jsx b/public/js/app/pages/CoursePage.jsx
index 66c94960..3b62f13b 100644
--- a/public/js/app/pages/CoursePage.jsx
+++ b/public/js/app/pages/CoursePage.jsx
@@ -19,36 +19,45 @@ import { useWebContext } from '../context/WebContext'
import BankIdAlert from '../components/BankIdAlert'
import { useLanguage } from '../hooks/useLanguage'
import { useMissingInfo } from '../hooks/useMissingInfo'
+import { useSemesterRoundState } from '../hooks/useSemesterRoundState'
+import { convertYearSemesterNumberIntoSemester } from '../../../../server/util/semesterUtils'
const aboutCourseStr = (translate, courseCode = '') => `${translate.site_name} ${courseCode}`
function CoursePage() {
- const [context, setWebContext] = useWebContext()
+ const context = useWebContext()
const {
- activeRoundIndex,
- activeSemester,
- activeSemesterIndex,
+ initiallySelectedRoundIndex,
+ initiallySelectedSemester,
activeSemesters,
- activeSyllabusIndex,
browserConfig,
courseCode,
courseData = {
courseInfo: { course_application_info: '' },
- syllabusSemesterList: [],
+ syllabusList: [],
},
- isCancelled,
- isDeactivated,
- roundInfoFade,
- showRoundData,
- syllabusInfoFade,
- useStartSemesterFromQuery,
+ isCancelledOrDeactivated,
} = context
// * * //
- const hasOnlyOneRound = activeSemester?.length > 0 && courseData.roundList[activeSemester].length === 1
- const hasToShowRoundsData = showRoundData || (useStartSemesterFromQuery && hasOnlyOneRound)
- const hasActiveSemesters = activeSemesters && activeSemesters.length > 0
+ const semesterRoundState = useSemesterRoundState({
+ initiallySelectedRoundIndex,
+ initiallySelectedSemester,
+ roundsBySemester: courseData.roundsBySemester,
+ syllabusList: courseData.syllabusList,
+ activeSemesters,
+ })
+ const {
+ selectedSemester,
+ firstRoundInActiveSemester,
+ activeRound,
+ showRoundData,
+ hasActiveSemesters,
+ activeSyllabus,
+ hasSyllabus,
+ } = semesterRoundState
+
const { courseInfo } = courseData
const { translation, languageShortname } = useLanguage()
@@ -69,18 +78,18 @@ function CoursePage() {
}
courseImage = `${browserConfig.imageStorageUri}${courseImage}`
- if (!courseData.syllabusList) courseData.syllabusList = [{}]
+ const decisionToDiscontinue = hasSyllabus ? activeSyllabus.course_decision_to_discontinue : ''
+ const course_valid_from = hasSyllabus ? activeSyllabus.course_valid_from : ''
+
const courseInformationToRounds = {
course_code: courseCode,
course_examiners: courseInfo.course_examiners,
course_contact_name: courseInfo.course_contact_name,
course_main_subject: courseInfo.course_main_subject,
course_level_code: courseInfo.course_level_code,
- course_valid_from: courseData.syllabusList[activeSyllabusIndex || 0].course_valid_from,
+ course_valid_from,
}
- const { course_decision_to_discontinue: decisionToDiscontinue = '' } =
- activeSyllabusIndex > -1 ? courseData.syllabusList[activeSyllabusIndex] : {}
useEffect(() => {
let isMounted = true
if (isMounted) {
@@ -91,23 +100,7 @@ function CoursePage() {
}
}
return () => (isMounted = false)
- }, [])
-
- useEffect(() => {
- let isMounted = true
- if (isMounted) {
- if (syllabusInfoFade) {
- setTimeout(() => {
- setWebContext({ ...context, roundInfoFade: false, syllabusInfoFade: false })
- }, 800)
- } else {
- setTimeout(() => {
- setWebContext({ ...context, roundInfoFade: false })
- }, 500)
- }
- }
- return () => (isMounted = false)
- }, [roundInfoFade, syllabusInfoFade])
+ })
return (
@@ -124,7 +117,7 @@ function CoursePage() {
pageTitle={translation.courseLabels.sideMenu.page_before_course}
/>
{/* ---TEXT FOR CANCELLED COURSE --- */}
- {(isCancelled || isDeactivated) && (
+ {isCancelledOrDeactivated && (
-
-
+
+
-
- {courseData.roundList && activeSemesters.length > 0 && hasToShowRoundsData && (
+ {showRoundData && (
0 && hasToShowRoundsData}
+ tutoringForm={activeRound.round_tutoring_form}
+ fundingType={activeRound.round_funding_type}
+ roundSpecified={hasActiveSemesters && showRoundData}
/>
)}
{/** ************************************************************************************************************ */}
{/* RIGHT COLUMN - ROUND INFORMATION */}
{/** ************************************************************************************************************ */}
-
+
{/* ---COURSE DROPDOWN MENU--- */}
{hasActiveSemesters ? (